From 71c06cf178f231bcb2d2284922210f4e6fb9b410 Mon Sep 17 00:00:00 2001 From: maelk Date: Mon, 11 May 2020 18:18:25 +0300 Subject: [PATCH 1/9] Initializing the repo --- .github/ISSUE_TEMPLATE/bug_report.md | 27 + .github/ISSUE_TEMPLATE/feature_request.md | 19 + .github/PULL_REQUEST_TEMPLATE.md | 7 + .gitignore | 18 + .mdlrc | 2 + CONTRIBUTING.md | 116 ++ DCO | 37 + Dockerfile | 47 + LICENSE | 201 ++ Makefile | 447 +++++ README.md | 80 +- VERSIONING.md | 178 ++ api/v1alpha1/common_types.go | 41 + api/v1alpha1/conversion.go | 21 + api/v1alpha1/doc.go | 23 + api/v1alpha1/groupversion_info.go | 47 + api/v1alpha1/ipaddress_types.go | 73 + api/v1alpha1/ipaddress_webhook.go | 158 ++ api/v1alpha1/ipaddress_webhook_test.go | 318 +++ api/v1alpha1/ipclaim_types.go | 72 + api/v1alpha1/ipclaim_webhook.go | 102 + api/v1alpha1/ipclaim_webhook_test.go | 218 +++ api/v1alpha1/ippool_types.go | 115 ++ api/v1alpha1/ippool_webhook.go | 84 + api/v1alpha1/ippool_webhook_test.go | 133 ++ api/v1alpha1/v1alpha4_suite_test.go | 55 + api/v1alpha1/zz_generated.deepcopy.go | 360 ++++ config/certmanager/certificate.yaml | 24 + config/certmanager/kustomization.yaml | 7 + config/certmanager/kustomizeconfig.yaml | 19 + .../ipam.metal3.io_metal3ipaddresses.yaml | 143 ++ .../bases/ipam.metal3.io_metal3ipclaims.yaml | 138 ++ .../bases/ipam.metal3.io_metal3ippools.yaml | 137 ++ config/crd/kustomization.yaml | 52 + config/crd/kustomizeconfig.yaml | 17 + .../cainjection_in_metal3clusters.yaml | 8 + .../cainjection_in_metal3dataclaims.yaml | 8 + .../patches/cainjection_in_metal3datas.yaml | 8 + .../cainjection_in_metal3datatemplates.yaml | 8 + .../cainjection_in_metal3ipaddresses.yaml | 8 + .../cainjection_in_metal3ipclaims.yaml | 8 + .../patches/cainjection_in_metal3ippools.yaml | 8 + .../cainjection_in_metal3machines.yaml | 8 + ...cainjection_in_metal3machinetemplates.yaml | 8 + .../patches/webhook_in_metal3clusters.yaml | 19 + .../patches/webhook_in_metal3dataclaims.yaml | 19 + .../crd/patches/webhook_in_metal3datas.yaml | 19 + .../webhook_in_metal3datatemplates.yaml | 19 + .../patches/webhook_in_metal3ipaddresses.yaml | 19 + .../patches/webhook_in_metal3ipclaims.yaml | 19 + .../crd/patches/webhook_in_metal3ippools.yaml | 19 + .../patches/webhook_in_metal3machines.yaml | 19 + .../webhook_in_metal3machinetemplates.yaml | 19 + config/default/kustomization.yaml | 12 + config/default/namespace.yaml | 4 + config/kustomization.yaml | 65 + config/manager/kustomization.yaml | 13 + config/manager/manager.yaml | 80 + config/manager/manager_auth_proxy_patch.yaml | 25 + config/manager/manager_image_patch.yaml | 12 + .../manager_prometheus_metrics_patch.yaml | 19 + config/manager/manager_pull_policy.yaml | 11 + config/patch_crd_webhook_namespace.yaml | 3 + config/rbac/auth_proxy_role.yaml | 13 + config/rbac/auth_proxy_role_binding.yaml | 12 + config/rbac/auth_proxy_service.yaml | 18 + config/rbac/kustomization.yaml | 10 + config/rbac/leader_election_role.yaml | 32 + config/rbac/leader_election_role_binding.yaml | 12 + config/rbac/role.yaml | 243 +++ config/rbac/role_binding.yaml | 12 + config/webhook/kustomization.yaml | 42 + config/webhook/kustomizeconfig.yaml | 27 + config/webhook/manager_webhook_patch.yaml | 26 + config/webhook/manifests.yaml | 130 ++ config/webhook/service.yaml | 10 + config/webhook/webhookcainjection_patch.yaml | 15 + controllers/metal3ippool_controller.go | 221 +++ controllers/metal3ippool_controller_test.go | 407 ++++ controllers/suite_test.go | 136 ++ docs/api.md | 928 +++++++++ docs/architecture.md | 459 +++++ docs/deployment_workflow.md | 86 + docs/dev-setup.md | 75 + docs/getting-started.md | 219 +++ docs/images/components.png | Bin 0 -> 90895 bytes docs/images/controllerssequencediagram.png | Bin 0 -> 55662 bytes docs/images/fields_mapping.png | Bin 0 -> 48250 bytes docs/releasing.md | 87 + docs/testing.md | 62 + examples/addons.yaml | 783 ++++++++ examples/cluster/cluster.yaml | 29 + examples/cluster/kustomization.yaml | 7 + examples/cluster/kustomizeconfig.yaml | 6 + .../clusterctl-cluster.yaml | 128 ++ .../clusterctl-templates/example_variables.rc | 105 + examples/controlplane/controlplane.yaml | 180 ++ examples/controlplane/kustomization.yaml | 7 + examples/controlplane/kustomizeconfig.yaml | 15 + examples/generate.sh | 128 ++ examples/machinedeployment/kustomization.yaml | 7 + .../machinedeployment/kustomizeconfig.yaml | 11 + .../machinedeployment/machinedeployment.yaml | 144 ++ examples/metal3crds/kustomization.yaml | 5 + .../metal3crds/metal3.io_baremetalhosts.yaml | 574 ++++++ examples/metal3plane/hosts.yaml | 749 ++++++++ examples/metal3plane/kustomization.yaml | 5 + .../provider-components/kustomization.yaml | 9 + .../manager_tolerations_patch.yaml | 56 + go.mod | 81 + go.sum | 1710 +++++++++++++++++ hack/Dockerfile.unit | 6 + hack/boilerplate.go.txt | 15 + hack/boilerplate/BUILD | 31 + hack/boilerplate/boilerplate.Dockerfile.txt | 14 + hack/boilerplate/boilerplate.Makefile.txt | 14 + hack/boilerplate/boilerplate.bzl.txt | 14 + hack/boilerplate/boilerplate.generatebzl.txt | 14 + hack/boilerplate/boilerplate.generatego.txt | 16 + hack/boilerplate/boilerplate.go.txt | 15 + hack/boilerplate/boilerplate.py | 226 +++ hack/boilerplate/boilerplate.py.txt | 16 + hack/boilerplate/boilerplate.sh.txt | 14 + hack/boilerplate/boilerplate_test.py | 52 + hack/boilerplate/test/BUILD | 28 + hack/boilerplate/test/fail.go | 19 + hack/boilerplate/test/fail.py | 17 + hack/boilerplate/test/pass.go | 17 + hack/boilerplate/test/pass.py | 17 + hack/ensure-go.sh | 48 + hack/ensure-kind.sh | 54 + hack/ensure-kubectl.sh | 54 + hack/ensure-kustomize.sh | 54 + hack/fetch_ext_bins.sh | 111 ++ hack/gofmt.sh | 22 + hack/govet.sh | 22 + hack/kind/config.yaml | 7 + hack/kind/start_controllers.sh | 44 + hack/markdownlint.sh | 19 + hack/shellcheck.sh | 19 + hack/tools/go.mod | 25 + hack/tools/go.sum | 664 +++++++ hack/tools/install_kubebuilder.sh | 15 + hack/tools/install_kustomize.sh | 14 + hack/tools/release/notes.go | 189 ++ hack/tools/tools.go | 29 + hack/unit.sh | 23 + hack/verify-boilerplate.sh | 39 + ipam/manager_factory.go | 44 + ipam/manager_factory_test.go | 48 + ipam/metal3ippool_manager.go | 520 +++++ ipam/metal3ippool_manager_test.go | 1157 +++++++++++ ipam/mocks/zz_generated.manager_factory.go | 69 + .../zz_generated.metal3ippool_manager.go | 106 + ipam/remote/remote.go | 46 + ipam/remote/remote_test.go | 118 ++ ipam/requeue_error.go | 49 + ipam/requeue_error_test.go | 43 + ipam/suite_test.go | 114 ++ ipam/utils.go | 163 ++ ipam/utils_test.go | 503 +++++ main.go | 312 +++ 162 files changed, 17332 insertions(+), 1 deletion(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .gitignore create mode 100644 .mdlrc create mode 100644 CONTRIBUTING.md create mode 100644 DCO create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 VERSIONING.md create mode 100644 api/v1alpha1/common_types.go create mode 100644 api/v1alpha1/conversion.go create mode 100644 api/v1alpha1/doc.go create mode 100644 api/v1alpha1/groupversion_info.go create mode 100644 api/v1alpha1/ipaddress_types.go create mode 100644 api/v1alpha1/ipaddress_webhook.go create mode 100644 api/v1alpha1/ipaddress_webhook_test.go create mode 100644 api/v1alpha1/ipclaim_types.go create mode 100644 api/v1alpha1/ipclaim_webhook.go create mode 100644 api/v1alpha1/ipclaim_webhook_test.go create mode 100644 api/v1alpha1/ippool_types.go create mode 100644 api/v1alpha1/ippool_webhook.go create mode 100644 api/v1alpha1/ippool_webhook_test.go create mode 100644 api/v1alpha1/v1alpha4_suite_test.go create mode 100644 api/v1alpha1/zz_generated.deepcopy.go create mode 100644 config/certmanager/certificate.yaml create mode 100644 config/certmanager/kustomization.yaml create mode 100644 config/certmanager/kustomizeconfig.yaml create mode 100644 config/crd/bases/ipam.metal3.io_metal3ipaddresses.yaml create mode 100644 config/crd/bases/ipam.metal3.io_metal3ipclaims.yaml create mode 100644 config/crd/bases/ipam.metal3.io_metal3ippools.yaml create mode 100644 config/crd/kustomization.yaml create mode 100644 config/crd/kustomizeconfig.yaml create mode 100644 config/crd/patches/cainjection_in_metal3clusters.yaml create mode 100644 config/crd/patches/cainjection_in_metal3dataclaims.yaml create mode 100644 config/crd/patches/cainjection_in_metal3datas.yaml create mode 100644 config/crd/patches/cainjection_in_metal3datatemplates.yaml create mode 100644 config/crd/patches/cainjection_in_metal3ipaddresses.yaml create mode 100644 config/crd/patches/cainjection_in_metal3ipclaims.yaml create mode 100644 config/crd/patches/cainjection_in_metal3ippools.yaml create mode 100644 config/crd/patches/cainjection_in_metal3machines.yaml create mode 100644 config/crd/patches/cainjection_in_metal3machinetemplates.yaml create mode 100644 config/crd/patches/webhook_in_metal3clusters.yaml create mode 100644 config/crd/patches/webhook_in_metal3dataclaims.yaml create mode 100644 config/crd/patches/webhook_in_metal3datas.yaml create mode 100644 config/crd/patches/webhook_in_metal3datatemplates.yaml create mode 100644 config/crd/patches/webhook_in_metal3ipaddresses.yaml create mode 100644 config/crd/patches/webhook_in_metal3ipclaims.yaml create mode 100644 config/crd/patches/webhook_in_metal3ippools.yaml create mode 100644 config/crd/patches/webhook_in_metal3machines.yaml create mode 100644 config/crd/patches/webhook_in_metal3machinetemplates.yaml create mode 100644 config/default/kustomization.yaml create mode 100644 config/default/namespace.yaml create mode 100644 config/kustomization.yaml create mode 100644 config/manager/kustomization.yaml create mode 100644 config/manager/manager.yaml create mode 100644 config/manager/manager_auth_proxy_patch.yaml create mode 100644 config/manager/manager_image_patch.yaml create mode 100644 config/manager/manager_prometheus_metrics_patch.yaml create mode 100644 config/manager/manager_pull_policy.yaml create mode 100644 config/patch_crd_webhook_namespace.yaml create mode 100644 config/rbac/auth_proxy_role.yaml create mode 100644 config/rbac/auth_proxy_role_binding.yaml create mode 100644 config/rbac/auth_proxy_service.yaml create mode 100644 config/rbac/kustomization.yaml create mode 100644 config/rbac/leader_election_role.yaml create mode 100644 config/rbac/leader_election_role_binding.yaml create mode 100644 config/rbac/role.yaml create mode 100644 config/rbac/role_binding.yaml create mode 100644 config/webhook/kustomization.yaml create mode 100644 config/webhook/kustomizeconfig.yaml create mode 100644 config/webhook/manager_webhook_patch.yaml create mode 100644 config/webhook/manifests.yaml create mode 100644 config/webhook/service.yaml create mode 100644 config/webhook/webhookcainjection_patch.yaml create mode 100644 controllers/metal3ippool_controller.go create mode 100644 controllers/metal3ippool_controller_test.go create mode 100644 controllers/suite_test.go create mode 100644 docs/api.md create mode 100644 docs/architecture.md create mode 100644 docs/deployment_workflow.md create mode 100644 docs/dev-setup.md create mode 100644 docs/getting-started.md create mode 100644 docs/images/components.png create mode 100644 docs/images/controllerssequencediagram.png create mode 100644 docs/images/fields_mapping.png create mode 100644 docs/releasing.md create mode 100644 docs/testing.md create mode 100644 examples/addons.yaml create mode 100644 examples/cluster/cluster.yaml create mode 100644 examples/cluster/kustomization.yaml create mode 100644 examples/cluster/kustomizeconfig.yaml create mode 100644 examples/clusterctl-templates/clusterctl-cluster.yaml create mode 100644 examples/clusterctl-templates/example_variables.rc create mode 100644 examples/controlplane/controlplane.yaml create mode 100644 examples/controlplane/kustomization.yaml create mode 100644 examples/controlplane/kustomizeconfig.yaml create mode 100755 examples/generate.sh create mode 100644 examples/machinedeployment/kustomization.yaml create mode 100644 examples/machinedeployment/kustomizeconfig.yaml create mode 100644 examples/machinedeployment/machinedeployment.yaml create mode 100644 examples/metal3crds/kustomization.yaml create mode 100644 examples/metal3crds/metal3.io_baremetalhosts.yaml create mode 100644 examples/metal3plane/hosts.yaml create mode 100644 examples/metal3plane/kustomization.yaml create mode 100644 examples/provider-components/kustomization.yaml create mode 100644 examples/provider-components/manager_tolerations_patch.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 hack/Dockerfile.unit create mode 100644 hack/boilerplate.go.txt create mode 100644 hack/boilerplate/BUILD create mode 100644 hack/boilerplate/boilerplate.Dockerfile.txt create mode 100644 hack/boilerplate/boilerplate.Makefile.txt create mode 100644 hack/boilerplate/boilerplate.bzl.txt create mode 100644 hack/boilerplate/boilerplate.generatebzl.txt create mode 100644 hack/boilerplate/boilerplate.generatego.txt create mode 100644 hack/boilerplate/boilerplate.go.txt create mode 100755 hack/boilerplate/boilerplate.py create mode 100644 hack/boilerplate/boilerplate.py.txt create mode 100644 hack/boilerplate/boilerplate.sh.txt create mode 100644 hack/boilerplate/boilerplate_test.py create mode 100644 hack/boilerplate/test/BUILD create mode 100644 hack/boilerplate/test/fail.go create mode 100644 hack/boilerplate/test/fail.py create mode 100644 hack/boilerplate/test/pass.go create mode 100644 hack/boilerplate/test/pass.py create mode 100755 hack/ensure-go.sh create mode 100755 hack/ensure-kind.sh create mode 100755 hack/ensure-kubectl.sh create mode 100755 hack/ensure-kustomize.sh create mode 100644 hack/fetch_ext_bins.sh create mode 100755 hack/gofmt.sh create mode 100755 hack/govet.sh create mode 100644 hack/kind/config.yaml create mode 100755 hack/kind/start_controllers.sh create mode 100755 hack/markdownlint.sh create mode 100755 hack/shellcheck.sh create mode 100644 hack/tools/go.mod create mode 100644 hack/tools/go.sum create mode 100755 hack/tools/install_kubebuilder.sh create mode 100755 hack/tools/install_kustomize.sh create mode 100644 hack/tools/release/notes.go create mode 100644 hack/tools/tools.go create mode 100755 hack/unit.sh create mode 100755 hack/verify-boilerplate.sh create mode 100644 ipam/manager_factory.go create mode 100644 ipam/manager_factory_test.go create mode 100644 ipam/metal3ippool_manager.go create mode 100644 ipam/metal3ippool_manager_test.go create mode 100644 ipam/mocks/zz_generated.manager_factory.go create mode 100644 ipam/mocks/zz_generated.metal3ippool_manager.go create mode 100644 ipam/remote/remote.go create mode 100644 ipam/remote/remote_test.go create mode 100644 ipam/requeue_error.go create mode 100644 ipam/requeue_error_test.go create mode 100644 ipam/suite_test.go create mode 100644 ipam/utils.go create mode 100644 ipam/utils_test.go create mode 100644 main.go diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..85e5a601 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,27 @@ +--- +name: Bug report +about: Tell us about a problem you are experiencing + +--- + +**What steps did you take and what happened:** +[A clear and concise description on how to REPRODUCE the bug.] + + +**What did you expect to happen:** + + +**Anything else you would like to add:** +[Miscellaneous information that will assist in solving the issue.] + + +**Environment:** + +- Cluster-api version: +- CAPM3 version: +- IPAM version: +- Minikube version: +- environment (metal3-dev-env or other): +- Kubernetes version: (use `kubectl version`): + +/kind bug diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..7f4340da --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature addition +about: Suggest and track an idea for this project + +--- + +**User Story** + +As a [developer/user/operator] I would like to [high level description] for [reasons] + +**Detailed Description** + +[A clear and concise description of what you want to happen.] + +**Anything else you would like to add:** + +[Miscellaneous information that will assist in solving the issue.] + +/kind feature diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..4765ecea --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,7 @@ + + + +**What this PR does / why we need it**: + +**Which issue(s) this PR fixes** *(optional, in `fixes #(, fixes #, ...)` format, will close the issue(s) when PR gets merged)*: +Fixes # diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..dd734a00 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +bin/* +hack/tools/bin/* +examples/_out/* +examples/provider-components/*-components.yaml +out/* diff --git a/.mdlrc b/.mdlrc new file mode 100644 index 00000000..1d4a4040 --- /dev/null +++ b/.mdlrc @@ -0,0 +1,2 @@ +#Disable line-length for some code examples in getting-started +rules "~MD013", "~MD005" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..b6098b4b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,116 @@ +# How to Contribute + +Metal3 projects are [Apache 2.0 licensed](LICENSE) and accept contributions via +GitHub pull requests. Those guidelines are the same as the +[Cluster API guidelines](https://github.com/kubernetes-sigs/cluster-api/blob/master/CONTRIBUTING.md) + + + + +- [Certificate of Origin](#certificate-of-origin) +- [Finding Things That Need Help](#finding-things-that-need-help) +- [Contributing a Patch](#contributing-a-patch) +- [Backporting a Patch](#backporting-a-patch) + - [Merge Approval](#merge-approval) + - [Google Doc Viewing Permissions](#google-doc-viewing-permissions) + - [Issue and Pull Request Management](#issue-and-pull-request-management) + + + +## Certificate of Origin + +By contributing to this project you agree to the Developer Certificate of +Origin (DCO). This document was created by the Linux Kernel community and is a +simple statement that you, as a contributor, have the legal right to make the +contribution. See the [DCO](DCO) file for details. + +## Finding Things That Need Help + +If you're new to the project and want to help, but don't know where to start, we +have a semi-curated list of issues that +should not need deep knowledge of the system. [Have a look and see if anything +sounds interesting](https://github.com/metal3-io/cluster-api-provider-metal3/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). +Alternatively, read some of the docs on other controllers and try to write your +own, file and fix any/all issues that come up, including gaps in documentation! + +## Contributing a Patch + +1. If you haven't already done so, sign a Contributor License Agreement (see + details above). +1. Fork the desired repo, develop and test your code changes. +1. Submit a pull request. + +All code PR must be labeled with one of + +- ⚠️ (:warning:, major or breaking changes) +- ✨ (:sparkles:, minor or feature additions) +- 🐛 (:bug:, patch and bugfixes) +- 📖 (:book:, documentation or proposals) +- 🏃 (:running:, other) + +All changes must be code reviewed. Coding conventions and standards are +explained in the official [developer +docs](https://github.com/kubernetes/community/tree/master/contributors/devel). +Expect reviewers to request that you +avoid common [go style +mistakes](https://github.com/golang/go/wiki/CodeReviewComments) in your PRs. + +## Backporting a Patch + +Cluster API maintains older versions through `release-X.Y` branches. We accept +backports of bug fixes to the most recent +release branch. For example, if the most recent branch is `release-0.2`, and the +`master` branch is under active +development for v0.3.0, a bug fix that merged to `master` that also affects +`v0.2.x` may be considered for backporting +to `release-0.2`. We generally do not accept PRs against older release branches. + +## Breaking Changes + +Breaking changes are generally allowed in the `master` branch, as this is the +branch used to develop the next minor release of Cluster API. + +There may be times, however, when `master` is closed for breaking changes. This +is likely to happen as we near the release of a new minor version. + +Breaking changes are not allowed in release branches, as these represent minor +versions that have already been released. +These versions have consumers who expect the APIs, behaviors, etc. to remain +stable during the life time of the patch stream for the minor release. + +Examples of breaking changes include: + +- Removing or renaming a field in a CRD +- Removing or renaming a CRD +- Removing or renaming an exported constant, variable, type, or function +- Updating the version of critical libraries such as controller-runtime, + client-go, apimachinery, etc. +- Some version updates may be acceptable, for picking up bug fixes, but + maintainers must exercise caution when reviewing. + +There may, at times, need to be exceptions where breaking changes are allowed in +release branches. These are at the discretion of the project's maintainers, and +must be carefully considered before merging. An example of an allowed +breaking change might be a fix for a behavioral bug that was released in an +initial minor version (such as `v0.3.0`). + +### Merge Approval + +Please see the [Kubernetes community document on pull +requests](https://git.k8s.io/community/contributors/guide/pull-requests.md) for +more information about the merge process. + +### Google Doc Viewing Permissions + +To gain viewing permissions to google docs in this project, please join the +[metal3-dev](https://groups.google.com/forum/#!forum/metal3-dev) google +group. + +### Issue and Pull Request Management + +Anyone may comment on issues and submit reviews for pull requests. However, in +order to be assigned an issue or pull request, you must be a member of the +[Metal3-io organization](https://github.com/metal3-io) GitHub organization. + +Metal3 maintainers can assign you an issue or pull request by leaving a +`/assign ` comment on the issue or pull request. diff --git a/DCO b/DCO new file mode 100644 index 00000000..8201f992 --- /dev/null +++ b/DCO @@ -0,0 +1,37 @@ +Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. +1 Letterman Drive +Suite D4700 +San Francisco, CA, 94129 + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..a44881d5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,47 @@ +# Copyright 2019 The Kubernetes 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. + +# Build the manager binary on golang image +FROM registry.hub.docker.com/library/golang:1.13 as builder +WORKDIR /workspace + +# Run this with docker build --build_arg $(go env GOPROXY) to override the goproxy +ARG goproxy=https://proxy.golang.org +ENV GOPROXY=$goproxy + +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum +# Cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN go mod download + +# Copy the sources +COPY main.go main.go +COPY api/ api/ +COPY ipam/ ipam/ +COPY controllers/ controllers/ + +# Build +ARG ARCH +RUN CGO_ENABLED=0 GOOS=linux GOARCH=${ARCH} \ + go build -a -ldflags '-extldflags "-static"' \ + -o manager . + +# Copy the controller-manager into a thin image +FROM gcr.io/distroless/static:latest +WORKDIR / +COPY --from=builder /workspace/manager . +USER nobody +ENTRYPOINT ["/manager"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..7279b847 --- /dev/null +++ b/Makefile @@ -0,0 +1,447 @@ +# Copyright 2018 The Kubernetes 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. + +# If you update this file, please follow +# https://suva.sh/posts/well-documented-makefiles + +# Ensure Make is run with bash shell as some syntax below is bash-specific +SHELL:=/usr/bin/env bash + +.DEFAULT_GOAL:=help + +# Use GOPROXY environment variable if set +GOPROXY := $(shell go env GOPROXY) +ifeq ($(GOPROXY),) +GOPROXY := https://proxy.golang.org +endif +export GOPROXY + +# Active module mode, as we use go modules to manage dependencies +export GO111MODULE=on + +# Directories. +TOOLS_DIR := hack/tools +TOOLS_BIN_DIR := $(TOOLS_DIR)/bin +BIN_DIR := bin + +# Binaries. +CLUSTERCTL := $(BIN_DIR)/clusterctl +CONTROLLER_GEN := $(TOOLS_BIN_DIR)/controller-gen +GOLANGCI_LINT := $(TOOLS_BIN_DIR)/golangci-lint +MOCKGEN := $(TOOLS_BIN_DIR)/mockgen +CONVERSION_GEN := $(TOOLS_BIN_DIR)/conversion-gen +KUBEBUILDER := $(TOOLS_BIN_DIR)/kubebuilder +KUSTOMIZE := $(TOOLS_BIN_DIR)/kustomize +RELEASE_NOTES_BIN := bin/release-notes +RELEASE_NOTES := $(TOOLS_DIR)/$(RELEASE_NOTES_BIN) + +# Define Docker related variables. Releases should modify and double check these vars. +# REGISTRY ?= gcr.io/$(shell gcloud config get-value project) +REGISTRY ?= quay.io/metal3-io +STAGING_REGISTRY := quay.io/metal3-io +PROD_REGISTRY := quay.io/metal3-io +IMAGE_NAME ?= ipam +CONTROLLER_IMG ?= $(REGISTRY)/$(IMAGE_NAME) +TAG ?= v1alpha1 +ARCH ?= amd64 +ALL_ARCH = amd64 arm arm64 ppc64le s390x + +# Allow overriding manifest generation destination directory +MANIFEST_ROOT ?= config +CRD_ROOT ?= $(MANIFEST_ROOT)/crd/bases +METAL3_CRD_ROOT ?= $(MANIFEST_ROOT)/crd/metal3 +WEBHOOK_ROOT ?= $(MANIFEST_ROOT)/webhook +RBAC_ROOT ?= $(MANIFEST_ROOT)/rbac + +# Allow overriding the imagePullPolicy +PULL_POLICY ?= IfNotPresent + +## -------------------------------------- +## Help +## -------------------------------------- + +help: ## Display this help + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +## -------------------------------------- +## Testing +## -------------------------------------- + +.PHONY: testprereqs +testprereqs: $(KUBEBUILDER) $(KUSTOMIZE) + +.PHONY: test +test: testprereqs generate fmt lint ## Run tests + source ./hack/fetch_ext_bins.sh; fetch_tools; setup_envs; go test -v ./api/... ./controllers/... ./ipam/... -coverprofile ./cover.out + +.PHONY: test-integration +test-integration: ## Run integration tests + source ./hack/fetch_ext_bins.sh; fetch_tools; setup_envs; go test -v -tags=integration ./test/integration/... + +.PHONY: test-e2e +test-e2e: ## Run e2e tests + PULL_POLICY=IfNotPresent $(MAKE) docker-build + go test -v -tags=e2e -timeout=1h ./test/e2e/... -args --managerImage $(CONTROLLER_IMG)-$(ARCH):$(TAG) + +## -------------------------------------- +## Binaries +## -------------------------------------- + +.PHONY: binaries +binaries: manager ## Builds and installs all binaries + +.PHONY: manager +manager: ## Build manager binary. + go build -o $(BIN_DIR)/manager . + +## -------------------------------------- +## Tooling Binaries +## -------------------------------------- + +$(CLUSTERCTL): go.mod ## Build clusterctl binary. + go build -o $(BIN_DIR)/clusterctl sigs.k8s.io/cluster-api/cmd/clusterctl + +$(CONTROLLER_GEN): $(TOOLS_DIR)/go.mod # Build controller-gen from tools folder. + cd $(TOOLS_DIR); go build -tags=tools -o $(BIN_DIR)/controller-gen sigs.k8s.io/controller-tools/cmd/controller-gen + +$(GOLANGCI_LINT): $(TOOLS_DIR)/go.mod # Build golangci-lint from tools folder. + cd $(TOOLS_DIR); go build -tags=tools -o $(BIN_DIR)/golangci-lint github.com/golangci/golangci-lint/cmd/golangci-lint + +$(MOCKGEN): $(TOOLS_DIR)/go.mod # Build mockgen from tools folder. + cd $(TOOLS_DIR); go build -tags=tools -o $(BIN_DIR)/mockgen github.com/golang/mock/mockgen + +$(CONVERSION_GEN): $(TOOLS_DIR)/go.mod + cd $(TOOLS_DIR); go build -tags=tools -o $(BIN_DIR)/conversion-gen k8s.io/code-generator/cmd/conversion-gen + +$(KUBEBUILDER): $(TOOLS_DIR)/go.mod + cd $(TOOLS_DIR); ./install_kubebuilder.sh + +$(KUSTOMIZE): $(TOOLS_DIR)/go.mod + cd $(TOOLS_DIR); ./install_kustomize.sh + +$(RELEASE_NOTES) : $(TOOLS_DIR)/go.mod + cd $(TOOLS_DIR) && go build -tags=tools -o $(RELEASE_NOTES_BIN) ./release + +## -------------------------------------- +## Linting +## -------------------------------------- + +.PHONY: lint +lint: $(GOLANGCI_LINT) ## Lint codebase + $(GOLANGCI_LINT) run -v + +lint-full: $(GOLANGCI_LINT) ## Run slower linters to detect possible issues + $(GOLANGCI_LINT) run -v --fast=false + +# Run go fmt against code +fmt: + go fmt ./api/... ./controllers/... ./ipam/... . + +# Run go vet against code +vet: + go vet ./api/... ./controllers/... ./ipam/... . + + +## -------------------------------------- +## Generate +## -------------------------------------- + +.PHONY: modules +modules: ## Runs go mod to ensure proper vendoring. + go mod tidy + cd $(TOOLS_DIR); go mod tidy + +.PHONY: generate +generate: ## Generate code + $(MAKE) generate-go + $(MAKE) generate-manifests + +.PHONY: generate-go +generate-go: $(CONTROLLER_GEN) $(MOCKGEN) $(CONVERSION_GEN) $(KUBEBUILDER) $(KUSTOMIZE) ## Runs Go related generate targets + go generate ./... + $(CONTROLLER_GEN) \ + paths=./api/... \ + object:headerFile=./hack/boilerplate/boilerplate.generatego.txt + + $(MOCKGEN) \ + -destination=./ipam/mocks/zz_generated.metal3ippool_manager.go \ + -source=./ipam/metal3ippool_manager.go \ + -package=ipam_mocks \ + -copyright_file=./hack/boilerplate/boilerplate.generatego.txt \ + IPPoolManagerInterface + + $(MOCKGEN) \ + -destination=./ipam/mocks/zz_generated.manager_factory.go \ + -source=./ipam/manager_factory.go \ + -package=ipam_mocks \ + -copyright_file=./hack/boilerplate/boilerplate.generatego.txt \ + ManagerFactoryInterface + +.PHONY: generate-manifests +generate-manifests: $(CONTROLLER_GEN) ## Generate manifests e.g. CRD, RBAC etc. + $(CONTROLLER_GEN) \ + paths=./api/... \ + crd:crdVersions=v1 \ + output:crd:dir=$(CRD_ROOT) \ + output:webhook:dir=$(WEBHOOK_ROOT) \ + webhook + $(CONTROLLER_GEN) \ + paths=./controllers/... \ + output:rbac:dir=$(RBAC_ROOT) \ + rbac:roleName=manager-role + +.PHONY: generate-examples +generate-examples: clean-examples ## Generate examples configurations to run a cluster. + ./examples/generate.sh + +## -------------------------------------- +## Docker +## -------------------------------------- + +.PHONY: docker-build +docker-build: ## Build the docker image for controller-manager + docker build --network=host --pull --build-arg ARCH=$(ARCH) . -t $(CONTROLLER_IMG)-$(ARCH):$(TAG) + MANIFEST_IMG=$(CONTROLLER_IMG)-$(ARCH) MANIFEST_TAG=$(TAG) $(MAKE) set-manifest-image + $(MAKE) set-manifest-pull-policy + +.PHONY: docker-push +docker-push: ## Push the docker image + docker push $(CONTROLLER_IMG)-$(ARCH):$(TAG) + +## -------------------------------------- +## Docker — All ARCH +## -------------------------------------- + +.PHONY: docker-build-all ## Build all the architecture docker images +docker-build-all: $(addprefix docker-build-,$(ALL_ARCH)) + +docker-build-%: + $(MAKE) ARCH=$* docker-build + +.PHONY: docker-push-all ## Push all the architecture docker images +docker-push-all: $(addprefix docker-push-,$(ALL_ARCH)) + $(MAKE) docker-push-manifest + +docker-push-%: + $(MAKE) ARCH=$* docker-push + +.PHONY: docker-push-manifest +docker-push-manifest: ## Push the fat manifest docker image. + ## Minimum docker version 18.06.0 is required for creating and pushing manifest images. + docker manifest create --amend $(CONTROLLER_IMG):$(TAG) $(shell echo $(ALL_ARCH) | sed -e "s~[^ ]*~$(CONTROLLER_IMG)\-&:$(TAG)~g") + @for arch in $(ALL_ARCH); do docker manifest annotate --arch $${arch} ${CONTROLLER_IMG}:${TAG} ${CONTROLLER_IMG}-$${arch}:${TAG}; done + docker manifest push --purge ${CONTROLLER_IMG}:${TAG} + MANIFEST_IMG=$(CONTROLLER_IMG) MANIFEST_TAG=$(TAG) $(MAKE) set-manifest-image + $(MAKE) set-manifest-pull-policy + +.PHONY: set-manifest-image +set-manifest-image: + $(info Updating kustomize image patch file for manager resource) + sed -i'' -e 's@image: .*@image: '"${MANIFEST_IMG}:$(MANIFEST_TAG)"'@' ./config/manager/manager_image_patch.yaml + + +.PHONY: set-manifest-pull-policy +set-manifest-pull-policy: + $(info Updating kustomize pull policy file for manager resource) + sed -i'' -e 's@imagePullPolicy: .*@imagePullPolicy: '"$(PULL_POLICY)"'@' ./config/manager/manager_pull_policy.yaml + +## -------------------------------------- +## Deploying +## -------------------------------------- + +# Run against the configured Kubernetes cluster in ~/.kube/config +run: generate fmt vet + go run ./main.go + +# Deploy controller in the configured Kubernetes cluster in ~/.kube/config +deploy: generate-examples + kubectl apply -f examples/_out/cert-manager.yaml + kubectl wait --for=condition=Available --timeout=300s -n cert-manager deployment cert-manager + kubectl wait --for=condition=Available --timeout=300s -n cert-manager deployment cert-manager-cainjector + kubectl wait --for=condition=Available --timeout=300s -n cert-manager deployment cert-manager-webhook + kubectl apply -f examples/_out/provider-components.yaml + +deploy-examples: + kubectl apply -f ./examples/_out/cluster.yaml + kubectl apply -f ./examples/_out/machinedeployment.yaml + kubectl apply -f ./examples/_out/controlplane.yaml + +delete-examples: + kubectl delete -f ./examples/_out/controlplane.yaml + kubectl delete -f ./examples/_out/machinedeployment.yaml + kubectl delete -f ./examples/_out/cluster.yaml + + +## -------------------------------------- +## Release +## -------------------------------------- + +RELEASE_TAG := $(shell git describe --abbrev=0 2>/dev/null) +RELEASE_DIR := out + +$(RELEASE_DIR): + mkdir -p $(RELEASE_DIR)/ + +.PHONY: release +release: clean-release ## Builds and push container images using the latest git tag for the commit. + @if [ -z "${RELEASE_TAG}" ]; then echo "RELEASE_TAG is not set"; exit 1; fi + # Push the release image to the staging bucket first. + #REGISTRY=$(STAGING_REGISTRY) TAG=$(RELEASE_TAG) \ + # $(MAKE) docker-build-all docker-push-all + # Set the manifest image to the production bucket. + MANIFEST_IMG=$(PROD_REGISTRY)/$(IMAGE_NAME) MANIFEST_TAG=$(RELEASE_TAG) \ + $(MAKE) set-manifest-image + PULL_POLICY=IfNotPresent $(MAKE) set-manifest-pull-policy + + $(MAKE) release-manifests + $(MAKE) release-binaries + +.PHONY: release-manifests +release-manifests: $(RELEASE_DIR) ## Builds the manifests to publish with a release + kustomize build config > $(RELEASE_DIR)/infrastructure-components.yaml + cp metadata.yaml $(RELEASE_DIR)/metadata.yaml + cp examples/clusterctl-templates/clusterctl-cluster.yaml $(RELEASE_DIR)/cluster-template.yaml + cp examples/clusterctl-templates/example_variables.rc $(RELEASE_DIR)/example_variables.rc + +.PHONY: release-binaries +release-binaries: ## Builds the binaries to publish with a release + +.PHONY: release-binary +release-binary: $(RELEASE_DIR) + docker run \ + --rm \ + -e CGO_ENABLED=0 \ + -e GOOS=$(GOOS) \ + -e GOARCH=$(GOARCH) \ + -v "$$(pwd):/workspace" \ + -w /workspace \ + golang:1.12.9 \ + go build -a -ldflags '-extldflags "-static"' \ + -o $(RELEASE_DIR)/$(notdir $(RELEASE_BINARY))-$(GOOS)-$(GOARCH) $(RELEASE_BINARY) + +.PHONY: release-staging +release-staging: ## Builds and push container images to the staging bucket. + REGISTRY=$(STAGING_REGISTRY) $(MAKE) docker-build-all docker-push-all release-tag-latest + + +.PHONY: release-tag-latest +release-tag-latest: ## Adds the latest tag to the last build tag. + ## TODO(vincepri): Only do this when we're on master. + gcloud container images add-tag $(CONTROLLER_IMG):$(TAG) $(CONTROLLER_IMG):latest + +.PHONY: release-notes +release-notes: $(RELEASE_NOTES) ## Generates a release notes template to be used with a release. + $(RELEASE_NOTES) + +## -------------------------------------- +## Development +## -------------------------------------- + +.PHONY: create-cluster +create-cluster: $(CLUSTERCTL) ## Create a development Kubernetes cluster using examples + $(CLUSTERCTL) \ + create cluster -v 4 \ + --bootstrap-flags="name=clusterapi" \ + --bootstrap-type kind \ + -m ./examples/_out/controlplane.yaml \ + -c ./examples/_out/cluster.yaml \ + -p ./examples/_out/provider-components.yaml \ + -a ./examples/addons.yaml + + +.PHONY: create-cluster-management +create-cluster-management: $(CLUSTERCTL) ## Create a development Kubernetes cluster in a KIND management cluster. + kind create cluster --name=clusterapi + # Apply provider-components. + kubectl \ + --kubeconfig=$$(kind get kubeconfig-path --name="clusterapi") \ + create -f examples/_out/provider-components.yaml + # Create Cluster. + kubectl \ + --kubeconfig=$$(kind get kubeconfig-path --name="clusterapi") \ + create -f examples/_out/cluster.yaml + # Create control plane machine. + kubectl \ + --kubeconfig=$$(kind get kubeconfig-path --name="clusterapi") \ + create -f examples/_out/controlplane.yaml + # Get KubeConfig using clusterctl. + $(CLUSTERCTL) \ + alpha phases get-kubeconfig -v=3 \ + --kubeconfig=$$(kind get kubeconfig-path --name="clusterapi") \ + --namespace=default \ + --cluster-name=test1 + # Apply addons on the target cluster, waiting for the control-plane to become available. + $(CLUSTERCTL) \ + alpha phases apply-addons -v=3 \ + --kubeconfig=./kubeconfig \ + -a examples/addons.yaml + # Create a worker node with MachineDeployment. + kubectl \ + --kubeconfig=$$(kind get kubeconfig-path --name="clusterapi") \ + create -f examples/_out/machinedeployment.yaml + +.PHONY: delete-cluster +delete-cluster: $(CLUSTERCTL) ## Deletes the development Kubernetes Cluster "test1" + $(CLUSTERCTL) \ + delete cluster -v 4 \ + --bootstrap-type kind \ + --bootstrap-flags="name=clusterapi" \ + --cluster test1 \ + --kubeconfig ./kubeconfig \ + -p ./examples/_out/provider-components.yaml \ + +.PHONY: kind-reset +kind-reset: ## Destroys the "clusterapi" kind cluster. + kind delete cluster --name=clusterapi || true + +## -------------------------------------- +## Cleanup / Verification +## -------------------------------------- + +.PHONY: clean +clean: ## Remove all generated files + $(MAKE) clean-bin + $(MAKE) clean-temporary + +.PHONY: clean-bin +clean-bin: ## Remove all generated binaries + rm -rf bin + rm -rf hack/tools/bin + +.PHONY: clean-temporary +clean-temporary: ## Remove all temporary files and folders + rm -f minikube.kubeconfig + rm -f kubeconfig + +.PHONY: clean-release +clean-release: ## Remove the release folder + rm -rf $(RELEASE_DIR) + +.PHONY: clean-examples +clean-examples: ## Remove all the temporary files generated in the examples folder + rm -rf examples/_out/ + rm -f examples/provider-components/provider-components-*.yaml + +.PHONY: verify +verify: verify-boilerplate verify-modules + +.PHONY: verify-boilerplate +verify-boilerplate: + ./hack/verify-boilerplate.sh + +.PHONY: verify-modules +verify-modules: modules + @if !(git diff --quiet HEAD -- go.sum go.mod hack/tools/go.mod hack/tools/go.sum); then \ + echo "go module files are out of date"; exit 1; \ + fi diff --git a/README.md b/README.md index 54b1f3e6..ad111397 100644 --- a/README.md +++ b/README.md @@ -1 +1,79 @@ -# ipam +# Cluster API Provider for Managed Bare Metal Hardware + +[![Ubuntu V1alpha3 build status](https://jenkins.nordix.org/view/Airship/job/airship_master_v1a3_integration_test_ubuntu/badge/icon?subject=Ubuntu%20E2E%20V1alpha3)](https://jenkins.nordix.org/view/Airship/job/airship_master_v1a3_integration_test_ubuntu) +[![CentOS V1alpha3 build status](https://jenkins.nordix.org/view/Airship/job/airship_master_v1a3_integration_test_centos/badge/icon?subject=CentOS%20E2E%20V1alpha3)](https://jenkins.nordix.org/view/Airship/job/airship_master_v1a3_integration_test_centos) + +This repository contains a Machine actuator implementation for the +Kubernetes [Cluster API](https://github.com/kubernetes-sigs/cluster-api/). + +For more information about this actuator and related repositories, see +[metal3.io](http://metal3.io/). + +## Compatibility with Cluster API + +| CAPM3 version | Cluster API version | Release | +|---------------|---------------------|---------| +| v1alpha2 | v1alpha2 | v0.2.X | +| v1alpha3 | v1alpha3 | v0.3.X | + +You can find information on how to use this provider with Cluster API and +clusterctl in the [getting-started](docs/getting-started.md). + +## Development Environment + +* See [metal3-dev-env](https://github.com/metal3-io/metal3-dev-env) for an + end-to-end development and test environment for + `cluster-api-provider-metal3` and + [baremetal-operator](https://github.com/metal3-io/baremetal-operator). +* [Setting up for tests](docs/dev-setup.md) + +## API + +See the [API Documentation](docs/api.md) for details about the objects used with +this `cluster-api` provider. You can also see the [cluster deployment +workflow](docs/deployment_workflow.md) for the outline of the +deployment process. + +## Architecture + +The architecture with the components involved is documented [here](docs/architecture.md) + +## Deployment and examples + +### Deploy Bare Metal Operator CRDs and CRs + +for testing purposes only, when Bare Metal Operator is not deployed + +```sh + make deploy-bmo-cr +``` + +### Deploy CAPM3 + +Deploys CAPM3 CRDs and deploys CAPI, CABPK, CACPK and CAPM3 controllers + +```sh + make deploy +``` + +### Run locally + +Runs CAPM3 controller locally + +```sh + kubectl scale -n capm3-system deployment.v1.apps/capm3-controller-manager \ + --replicas 0 + make run +``` + +### Deploy an example cluster + +```sh + make deploy-examples +``` + +### Delete the example cluster + +```sh + make delete-examples +``` diff --git a/VERSIONING.md b/VERSIONING.md new file mode 100644 index 00000000..1faad20b --- /dev/null +++ b/VERSIONING.md @@ -0,0 +1,178 @@ +# Versioning + +Those guidelines are coming from +[Cluster API](https://github.com/kubernetes-sigs/cluster-api/blob/master/VERSIONING.md) +as we try to follow closely the release process + + +## TL;DR: + + +- We follow [Semantic Versioning (semver)](https://semver.org/). +- We try to follow Cluster API release cadence +- The cadence is subject to change if necessary, refer to the + [Milestones](https://github.com/kubernetes-sigs/cluster-api/milestones) page + for up-to-date information. +- The _master_ branch is where development happens, this might include breaking + changes. +- The _release-X_ branches contain stable, backward compatible code. A new + _release-X_ branch is created at every major (X) release. + +## Overview + +Cluster API follows [Semantic Versioning](https://semver.org). +I'd recommend reading the aforementioned link if you're not familiar, +but essentially, for any given release X.Y.Z: + +- an X (*major*) release indicates a set of backwards-compatible code. + Changing X means there's a breaking change. + +- a Y (*minor*) release indicates a minimum feature set. Changing Y means + the addition of a backwards-compatible feature. + +- a Z (*patch*) release indicates minimum set of bugfixes. Changing + Z means a backwards-compatible change that doesn't add functionality. + +*NB*: If the major release is `0`, any minor release may contain breaking +changes. + +These guarantees extend to all code exposed in public APIs of +Cluster API Provider Metal3. This includes code both in Cluster API Provider +Baremetal itself, *plus types from dependencies in public APIs*. Types and +functions not in public APIs are not considered part of the guarantee. + +In order to easily maintain the guarantees, we have a couple of processes +that we follow. + +## Branches + +Cluster API Provider Metal3 contains two types of branches: the *master* +branch and *release-X* branches. + +The *master* branch is where development happens. All the latest and +greatest code, including breaking changes, happen on master. + +The *release-X* branches contain stable, backwards compatible code. Every +major (X) release, a new such branch is created. It is from these +branches that minor and patch releases are tagged. In some cases, it may +be necessary to open PRs for bugfixes directly against stable branches, but +this should generally be avoided. + +The maintainers are responsible for updating the contents of this branch; +generally, this is done just before a release using release tooling that +filters and checks for changes tagged as breaking (see below). + +## PR Process + +Every PR should be annotated with an icon indicating whether it's +a: + +- Breaking change: :warning: (`:warning:`) +- Non-breaking feature: :sparkles: (`:sparkles:`) +- Patch fix: :bug: (`:bug:`) +- Docs: :book: (`:book:`) +- Infra/Tests/Other: :running: (`:running:`) + +You can also use the equivalent emoji directly, since GitHub doesn't +render the `:xyz:` aliases in PR titles. + +Individual commits should not be tagged separately, but will generally be +assumed to match the PR. For instance, if you have a bug fix in with +a breaking change, it's generally encouraged to submit the bug fix +separately, but if you must put them in one PR, mark the commit +separately. + +### Commands and Workflow + +Cluster API Provider Metal3 follows the standard Kubernetes workflow: any PR +needs `lgtm` and `approved` labels, PRs authors must have signed the CNCF CLA, +and PRs must pass the tests before being merged. See [the contributor +docs](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#the-testing-and-merge-workflow) +for more info. + +We use the same priority and kind labels as Kubernetes. See the labels +tab in GitHub for the full list. + +The standard Kubernetes comment commands should work in +Cluster API Provider Metal3. See [Prow](https://prow.k8s.io/command-help) +for a command reference. + +## Release Process + +Minor and patch releases are generally done immediately after a feature or +bugfix is landed, or sometimes a series of features tied together. + +Minor releases will only be tagged on the *most recent* major release +branch, except in exceptional circumstances. Patches will be backported +to maintained stable versions, as needed. + +Major releases are done shortly after a breaking change is merged -- once +a breaking change is merged, the next release *must* be a major revision. +We don't intend to have a lot of these, so we may put off merging breaking +PRs until a later date. + +### Exact Steps + +Refer to the [releasing document](./docs/releasing.md) for the exact steps. + +### Breaking Changes + +Try to avoid breaking changes. They make life difficult for users, who +have to rewrite their code when they eventually upgrade, and for +maintainers/contributors, who have to deal with differences between master +and stable branches. + +That being said, we'll occasionally want to make breaking changes. They'll +be merged onto master, and will then trigger a major release (see [Release +Process](#release-process)). Because breaking changes induce a major +revision, the maintainers may delay a particular breaking change until +a later date when they are ready to make a major revision with a few +breaking changes. + +If you're going to make a breaking change, please make sure to explain in +detail why it's helpful. Is it necessary to cleanly resolve an issue? +Does it improve API ergonomics? + +Maintainers should treat breaking changes with caution, and evaluate +potential non-breaking solutions (see below). + +Note that API breakage in public APIs due to dependencies will trigger +a major revision, so you may occasionally need to have a major release +anyway, due to changes in libraries like `k8s.io/client-go` or +`k8s.io/apimachinery`. + + +## Why don't we... + + +### Use "next"-style branches + +Development branches: + +- don't win us much in terms of maintenance in the case of breaking + changes (we still have to merge/manage multiple branches for development + and stable) + +- can be confusing to contributors, who often expect master to have the + latest changes. + +### Never break compatibility + +Never doing a new major release could be an admirable goal, but gradually +leads to API cruft. + +Since one of the goals of Cluster API is to be a friendly and +intuitive API, we want to avoid too much API cruft over time, and +occaisonal breaking changes in major releases help accomplish that goal. + +Furthermore, our dependency on Kubernetes libraries makes this difficult +(see below) + +### Always assume we've broken compatibility + +*a.k.a. k8s.io/client-go style* While this makes life easier (a bit) for +maintainers, it's problematic for users. While breaking changes arrive sooner, +upgrading becomes very painful. + +Furthermore, we still have to maintain stable branches for bugfixes, so +the maintenance burden isn't lessened by a ton. diff --git a/api/v1alpha1/common_types.go b/api/v1alpha1/common_types.go new file mode 100644 index 00000000..6ca283ab --- /dev/null +++ b/api/v1alpha1/common_types.go @@ -0,0 +1,41 @@ +/* +Copyright 2019 The Kubernetes 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 v1alpha1 + +// +kubebuilder:validation:Pattern="((^((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$)|(^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$))" +// IPAddress is used for validation of an IP address +type IPAddressStr string + +// +kubebuilder:validation:Pattern="^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$" +// IPAddressv6 is used for validation of an IPv6 address +type IPAddressv6Str string + +// +kubebuilder:validation:Pattern="^((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$" +// IPAddressv4 is used for validation of an IPv6 address +type IPAddressv4Str string + +// +kubebuilder:validation:Pattern="((^((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))/([0-9]|[1-2][0-9]|3[0-2])$)|(^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))/([0-9]|[0-9][0-9]|1[0-1][0-9]|12[0-8])$))" +// IPSubnet is used for validation of an IP subnet +type IPSubnetStr string + +// +kubebuilder:validation:Pattern="^((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))/([0-9]|[1-2][0-9]|3[0-2])$" +// IPSubnetv4 is used for validation of an IP subnet +type IPSubnetv4Str string + +// +kubebuilder:validation:Pattern="^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))/([0-9]|[0-9][0-9]|1[0-1][0-9]|12[0-8])$" +// IPSubnetv6 is used for validation of an IP subnet +type IPSubnetv6Str string diff --git a/api/v1alpha1/conversion.go b/api/v1alpha1/conversion.go new file mode 100644 index 00000000..5f383c17 --- /dev/null +++ b/api/v1alpha1/conversion.go @@ -0,0 +1,21 @@ +/* +Copyright 2019 The Kubernetes 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 v1alpha1 + +func (*Metal3IPPool) Hub() {} +func (*Metal3IPAddress) Hub() {} +func (*Metal3IPClaim) Hub() {} diff --git a/api/v1alpha1/doc.go b/api/v1alpha1/doc.go new file mode 100644 index 00000000..ca3b1471 --- /dev/null +++ b/api/v1alpha1/doc.go @@ -0,0 +1,23 @@ +/* +Copyright 2019 The Kubernetes 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 v1alpha1 contains API Schema definitions for the metal3 v1alpha1 API group +// +k8s:openapi-gen=true +// +k8s:deepcopy-gen=package,register +// +k8s:defaulter-gen=TypeMeta +// +kubebuilder:object:generate=true +// +groupName=ipam.metal3.io +package v1alpha1 diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go new file mode 100644 index 00000000..e2f247f2 --- /dev/null +++ b/api/v1alpha1/groupversion_info.go @@ -0,0 +1,47 @@ +/* +Copyright 2019 The Kubernetes 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 v1alpha1 contains API Schema definitions for the infrastructure v1alpha1 API group +// +kubebuilder:object:generate=true +// +k8s:openapi-gen=true +// +k8s:deepcopy-gen=package,register +// +k8s:defaulter-gen=TypeMeta +// +groupName=ipam.metal3.io +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "ipam.metal3.io", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme + + // localSchemeBuilder is for automatically generated conversions + // localSchemeBuilder = SchemeBuilder.SchemeBuilder +) + +// Resource is required by pkg/client/listers/... +// func Resource(resource string) schema.GroupResource { +// return SchemeGroupVersion.WithResource(resource).GroupResource() +// } diff --git a/api/v1alpha1/ipaddress_types.go b/api/v1alpha1/ipaddress_types.go new file mode 100644 index 00000000..f8118eba --- /dev/null +++ b/api/v1alpha1/ipaddress_types.go @@ -0,0 +1,73 @@ +/* +Copyright 2019 The Kubernetes 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 v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // DataFinalizer allows Metal3IPAddressReconciler to clean up resources + // associated with Metal3IPAddress before removing it from the apiserver. + IPAddressFinalizer = "metal3ipaddress.infrastructure.cluster.x-k8s.io" +) + +// Metal3IPAddressSpec defines the desired state of Metal3IPAddress. +type Metal3IPAddressSpec struct { + + // Claim points to the object the Metal3IPClaim was created for. + Claim corev1.ObjectReference `json:"claim"` + + // Pool is the Metal3IPPool this was generated from. + Pool corev1.ObjectReference `json:"pool"` + + // +kubebuilder:validation:Maximum=128 + // Prefix is the mask of the network as integer (max 128) + Prefix int `json:"prefix,omitempty"` + + // Gateway is the gateway ip address + Gateway *IPAddressStr `json:"gateway,omitempty"` + + // Address contains the IP address + Address IPAddressStr `json:"address"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:path=metal3ipaddresses,scope=Namespaced,categories=cluster-api,shortName=m3ipa;m3ipaddress;metal3ipaddress +// +kubebuilder:storageversion +// +kubebuilder:object:root=true +// Metal3IPAddress is the Schema for the metal3ipaddresses API +type Metal3IPAddress struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec Metal3IPAddressSpec `json:"spec,omitempty"` +} + +// +kubebuilder:object:root=true + +// Metal3IPAddressList contains a list of Metal3IPAddress +type Metal3IPAddressList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Metal3IPAddress `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Metal3IPAddress{}, &Metal3IPAddressList{}) +} diff --git a/api/v1alpha1/ipaddress_webhook.go b/api/v1alpha1/ipaddress_webhook.go new file mode 100644 index 00000000..ec284de2 --- /dev/null +++ b/api/v1alpha1/ipaddress_webhook.go @@ -0,0 +1,158 @@ +/* +Copyright 2020 The Kubernetes 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 v1alpha1 + +import ( + "github.com/pkg/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +func (c *Metal3IPAddress) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(c). + Complete() +} + +// +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1alpha4-metal3ipaddress,mutating=false,failurePolicy=fail,groups=infrastructure.cluster.x-k8s.io,resources=metal3ipaddresses,versions=v1alpha4,name=validation.metal3ipaddress.infrastructure.cluster.x-k8s.io,matchPolicy=Equivalent +// +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1alpha4-metal3ipaddress,mutating=true,failurePolicy=fail,groups=infrastructure.cluster.x-k8s.io,resources=metal3ipaddresses,versions=v1alpha4,name=default.metal3ipaddress.infrastructure.cluster.x-k8s.io,matchPolicy=Equivalent + +var _ webhook.Defaulter = &Metal3IPAddress{} +var _ webhook.Validator = &Metal3IPAddress{} + +func (c *Metal3IPAddress) Default() { +} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (c *Metal3IPAddress) ValidateCreate() error { + allErrs := field.ErrorList{} + if c.Spec.Pool.Name == "" { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "pool", "name"), + c.Spec.Pool.Name, + "cannot be empty", + ), + ) + } + + if c.Spec.Claim.Name == "" { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "claim", "name"), + c.Spec.Claim.Name, + "cannot be empty", + ), + ) + } + + if c.Spec.Address == "" { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "address"), + c.Spec.Address, + "cannot be empty", + ), + ) + } + + if len(allErrs) == 0 { + return nil + } + return apierrors.NewInvalid(GroupVersion.WithKind("Metal3IPAddress").GroupKind(), c.Name, allErrs) +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (c *Metal3IPAddress) ValidateUpdate(old runtime.Object) error { + allErrs := field.ErrorList{} + oldMetal3IPAddress, ok := old.(*Metal3IPAddress) + if !ok || oldMetal3IPAddress == nil { + return apierrors.NewInternalError(errors.New("unable to convert existing object")) + } + + if c.Spec.Address != oldMetal3IPAddress.Spec.Address { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "address"), + c.Spec.Address, + "cannot be modified", + ), + ) + } + + if c.Spec.Pool.Name != oldMetal3IPAddress.Spec.Pool.Name { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "pool"), + c.Spec.Pool, + "cannot be modified", + ), + ) + } else if c.Spec.Pool.Namespace != oldMetal3IPAddress.Spec.Pool.Namespace { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "pool"), + c.Spec.Pool, + "cannot be modified", + ), + ) + } else if c.Spec.Pool.Kind != oldMetal3IPAddress.Spec.Pool.Kind { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "pool"), + c.Spec.Pool, + "cannot be modified", + ), + ) + } + + if c.Spec.Claim.Name != oldMetal3IPAddress.Spec.Claim.Name { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "claim"), + c.Spec.Claim, + "cannot be modified", + ), + ) + } else if c.Spec.Claim.Namespace != oldMetal3IPAddress.Spec.Claim.Namespace { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "claim"), + c.Spec.Claim, + "cannot be modified", + ), + ) + } else if c.Spec.Claim.Kind != oldMetal3IPAddress.Spec.Claim.Kind { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "claim"), + c.Spec.Claim, + "cannot be modified", + ), + ) + } + + if len(allErrs) == 0 { + return nil + } + return apierrors.NewInvalid(GroupVersion.WithKind("Metal3IPAddress").GroupKind(), c.Name, allErrs) +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (c *Metal3IPAddress) ValidateDelete() error { + return nil +} diff --git a/api/v1alpha1/ipaddress_webhook_test.go b/api/v1alpha1/ipaddress_webhook_test.go new file mode 100644 index 00000000..fc4ac7bf --- /dev/null +++ b/api/v1alpha1/ipaddress_webhook_test.go @@ -0,0 +1,318 @@ +/* +Copyright 2019 The Kubernetes 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 v1alpha1 + +import ( + "testing" + + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestMetal3IPAddressDefault(t *testing.T) { + g := NewWithT(t) + + c := &Metal3IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + }, + Spec: Metal3IPAddressSpec{}, + } + c.Default() + + g.Expect(c.Spec).To(Equal(Metal3IPAddressSpec{})) +} + +func TestMetal3IPAddressCreateValidation(t *testing.T) { + + tests := []struct { + name string + addressName string + expectErr bool + ipPool corev1.ObjectReference + address IPAddressStr + claim corev1.ObjectReference + }{ + { + name: "should succeed when values and ipPools correct", + expectErr: false, + addressName: "abc-1", + ipPool: corev1.ObjectReference{ + Name: "abc", + }, + claim: corev1.ObjectReference{ + Name: "abc", + }, + address: "abcd", + }, + { + name: "should fail without address", + expectErr: true, + addressName: "abc-1", + ipPool: corev1.ObjectReference{ + Name: "abc", + }, + claim: corev1.ObjectReference{ + Name: "abc", + }, + }, + { + name: "should fail without ipPool name", + expectErr: true, + addressName: "abc-1", + ipPool: corev1.ObjectReference{ + Namespace: "abc", + }, + claim: corev1.ObjectReference{ + Name: "abc", + }, + address: "abcd", + }, + { + name: "should fail without claim name", + expectErr: true, + addressName: "abc-1", + ipPool: corev1.ObjectReference{ + Name: "abc", + }, + claim: corev1.ObjectReference{ + Namespace: "abc", + }, + address: "abcd", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + obj := &Metal3IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + Name: tt.addressName, + }, + Spec: Metal3IPAddressSpec{ + Pool: tt.ipPool, + Address: tt.address, + Claim: tt.claim, + }, + } + + if tt.expectErr { + g.Expect(obj.ValidateCreate()).NotTo(Succeed()) + } else { + g.Expect(obj.ValidateCreate()).To(Succeed()) + } + + g.Expect(obj.ValidateDelete()).To(Succeed()) + }) + } +} + +func TestMetal3IPAddressUpdateValidation(t *testing.T) { + + tests := []struct { + name string + expectErr bool + new *Metal3IPAddressSpec + old *Metal3IPAddressSpec + }{ + { + name: "should succeed when values are the same", + expectErr: false, + new: &Metal3IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + }, + Claim: corev1.ObjectReference{ + Name: "abc", + }, + Address: "abcd", + }, + old: &Metal3IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + }, + Claim: corev1.ObjectReference{ + Name: "abc", + }, + Address: "abcd", + }, + }, + { + name: "should fail with nil old", + expectErr: true, + new: &Metal3IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + }, + Address: "abcd", + }, + old: nil, + }, + { + name: "should fail when index changes", + expectErr: true, + new: &Metal3IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + }, + Address: "abcd", + }, + old: &Metal3IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + }, + Address: "abcde", + }, + }, + { + name: "should fail when pool name changes", + expectErr: true, + new: &Metal3IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + }, + Address: "abcd", + }, + old: &Metal3IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abcd", + }, + Address: "abcd", + }, + }, + { + name: "should fail when Pool Namespace changes", + expectErr: true, + new: &Metal3IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "abc", + }, + Address: "abcd", + }, + old: &Metal3IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "abcd", + }, + Address: "abcd", + }, + }, + { + name: "should fail when Pool kind changes", + expectErr: true, + new: &Metal3IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Kind: "abc", + }, + Address: "abcd", + }, + old: &Metal3IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Kind: "abcd", + }, + Address: "abcd", + }, + }, + { + name: "should fail when Claim name changes", + expectErr: true, + new: &Metal3IPAddressSpec{ + Claim: corev1.ObjectReference{ + Name: "abc", + }, + Address: "abcd", + }, + old: &Metal3IPAddressSpec{ + Claim: corev1.ObjectReference{ + Name: "abcd", + }, + Address: "abcd", + }, + }, + { + name: "should fail when Claim Namespace changes", + expectErr: true, + new: &Metal3IPAddressSpec{ + Claim: corev1.ObjectReference{ + Name: "abc", + Namespace: "abc", + }, + Address: "abcd", + }, + old: &Metal3IPAddressSpec{ + Claim: corev1.ObjectReference{ + Name: "abc", + Namespace: "abcd", + }, + Address: "abcd", + }, + }, + { + name: "should fail when Claim kind changes", + expectErr: true, + new: &Metal3IPAddressSpec{ + Claim: corev1.ObjectReference{ + Name: "abc", + Kind: "abc", + }, + Address: "abcd", + }, + old: &Metal3IPAddressSpec{ + Claim: corev1.ObjectReference{ + Name: "abc", + Kind: "abcd", + }, + Address: "abcd", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var new, old *Metal3IPAddress + g := NewWithT(t) + new = &Metal3IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + Name: "abc-1", + }, + Spec: *tt.new, + } + + if tt.old != nil { + old = &Metal3IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + Name: "abc-1", + }, + Spec: *tt.old, + } + } else { + old = nil + } + + if tt.expectErr { + g.Expect(new.ValidateUpdate(old)).NotTo(Succeed()) + } else { + g.Expect(new.ValidateUpdate(old)).To(Succeed()) + } + }) + } +} diff --git a/api/v1alpha1/ipclaim_types.go b/api/v1alpha1/ipclaim_types.go new file mode 100644 index 00000000..095ab814 --- /dev/null +++ b/api/v1alpha1/ipclaim_types.go @@ -0,0 +1,72 @@ +/* +Copyright 2019 The Kubernetes 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 v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // IPClaimFinalizer allows Metal3IPClaimReconciler to clean up resources + // associated with Metal3IPClaim before removing it from the apiserver. + IPClaimFinalizer = "metal3ipclaim.infrastructure.cluster.x-k8s.io" +) + +// Metal3IPClaimSpec defines the desired state of Metal3IPClaim. +type Metal3IPClaimSpec struct { + + // Pool is the Metal3IPPool this was generated from. + Pool corev1.ObjectReference `json:"pool"` +} + +// Metal3IPClaimStatus defines the observed state of Metal3IPClaim. +type Metal3IPClaimStatus struct { + + // Address is the Metal3IPAddress that was generated for this claim. + Address *corev1.ObjectReference `json:"address,omitempty"` + + // ErrorMessage contains the error message + ErrorMessage *string `json:"errorMessage,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:path=metal3ipclaims,scope=Namespaced,categories=cluster-api,shortName=m3ipc;m3ipclaim;metal3ipclaim +// +kubebuilder:storageversion +// +kubebuilder:subresource:status +// +kubebuilder:object:root=true +// Metal3IPClaim is the Schema for the metal3ipclaims API +type Metal3IPClaim struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec Metal3IPClaimSpec `json:"spec,omitempty"` + Status Metal3IPClaimStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// Metal3IPClaimList contains a list of Metal3IPClaim +type Metal3IPClaimList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Metal3IPClaim `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Metal3IPClaim{}, &Metal3IPClaimList{}) +} diff --git a/api/v1alpha1/ipclaim_webhook.go b/api/v1alpha1/ipclaim_webhook.go new file mode 100644 index 00000000..3e1ca637 --- /dev/null +++ b/api/v1alpha1/ipclaim_webhook.go @@ -0,0 +1,102 @@ +/* +Copyright 2020 The Kubernetes 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 v1alpha1 + +import ( + "github.com/pkg/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +func (c *Metal3IPClaim) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(c). + Complete() +} + +// +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1alpha4-metal3ipclaim,mutating=false,failurePolicy=fail,groups=infrastructure.cluster.x-k8s.io,resources=metal3ipclaims,versions=v1alpha4,name=validation.metal3ipclaim.infrastructure.cluster.x-k8s.io,matchPolicy=Equivalent +// +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1alpha4-metal3ipclaim,mutating=true,failurePolicy=fail,groups=infrastructure.cluster.x-k8s.io,resources=metal3ipclaims,versions=v1alpha4,name=default.metal3ipclaim.infrastructure.cluster.x-k8s.io,matchPolicy=Equivalent + +var _ webhook.Defaulter = &Metal3IPClaim{} +var _ webhook.Validator = &Metal3IPClaim{} + +func (c *Metal3IPClaim) Default() { +} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (c *Metal3IPClaim) ValidateCreate() error { + allErrs := field.ErrorList{} + if c.Spec.Pool.Name == "" { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "pool", "name"), + c.Spec.Pool.Name, + "cannot be empty", + ), + ) + } + + if len(allErrs) == 0 { + return nil + } + return apierrors.NewInvalid(GroupVersion.WithKind("Metal3IPClaim").GroupKind(), c.Name, allErrs) +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (c *Metal3IPClaim) ValidateUpdate(old runtime.Object) error { + allErrs := field.ErrorList{} + oldMetal3IPClaim, ok := old.(*Metal3IPClaim) + if !ok || oldMetal3IPClaim == nil { + return apierrors.NewInternalError(errors.New("unable to convert existing object")) + } + + if c.Spec.Pool.Name != oldMetal3IPClaim.Spec.Pool.Name { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "pool"), + c.Spec.Pool, + "cannot be modified", + ), + ) + } else if c.Spec.Pool.Namespace != oldMetal3IPClaim.Spec.Pool.Namespace { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "pool"), + c.Spec.Pool, + "cannot be modified", + ), + ) + } else if c.Spec.Pool.Kind != oldMetal3IPClaim.Spec.Pool.Kind { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "pool"), + c.Spec.Pool, + "cannot be modified", + ), + ) + } + + if len(allErrs) == 0 { + return nil + } + return apierrors.NewInvalid(GroupVersion.WithKind("Metal3IPClaim").GroupKind(), c.Name, allErrs) +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (c *Metal3IPClaim) ValidateDelete() error { + return nil +} diff --git a/api/v1alpha1/ipclaim_webhook_test.go b/api/v1alpha1/ipclaim_webhook_test.go new file mode 100644 index 00000000..45c474b3 --- /dev/null +++ b/api/v1alpha1/ipclaim_webhook_test.go @@ -0,0 +1,218 @@ +/* +Copyright 2019 The Kubernetes 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 v1alpha1 + +import ( + "testing" + + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestMetal3IPClaimDefault(t *testing.T) { + g := NewWithT(t) + + c := &Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + }, + } + c.Default() + + g.Expect(c.Spec).To(Equal(Metal3IPClaimSpec{})) + g.Expect(c.Status).To(Equal(Metal3IPClaimStatus{})) +} + +func TestMetal3IPClaimCreateValidation(t *testing.T) { + + tests := []struct { + name string + claimName string + expectErr bool + ipPool corev1.ObjectReference + }{ + { + name: "should succeed when ipPool is correct", + expectErr: false, + claimName: "abc-1", + ipPool: corev1.ObjectReference{ + Name: "abc", + }, + }, + { + name: "should fail without ipPool", + expectErr: true, + claimName: "abc-1", + ipPool: corev1.ObjectReference{}, + }, + { + name: "should fail without ipPool name", + expectErr: true, + claimName: "abc-1", + ipPool: corev1.ObjectReference{ + Namespace: "abc", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + obj := &Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + Name: tt.claimName, + }, + Spec: Metal3IPClaimSpec{ + Pool: tt.ipPool, + }, + } + + if tt.expectErr { + g.Expect(obj.ValidateCreate()).NotTo(Succeed()) + } else { + g.Expect(obj.ValidateCreate()).To(Succeed()) + } + + g.Expect(obj.ValidateDelete()).To(Succeed()) + }) + } +} + +func TestMetal3IPClaimUpdateValidation(t *testing.T) { + + tests := []struct { + name string + expectErr bool + new *Metal3IPClaimSpec + old *Metal3IPClaimSpec + }{ + { + name: "should succeed when values are the same", + expectErr: false, + new: &Metal3IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + }, + }, + old: &Metal3IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + }, + }, + }, + { + name: "should fail with nil old", + expectErr: true, + new: &Metal3IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + }, + }, + old: nil, + }, + { + name: "should fail when pool is unset", + expectErr: true, + new: &Metal3IPClaimSpec{ + Pool: corev1.ObjectReference{}, + }, + old: &Metal3IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + }, + }, + }, + { + name: "should fail when pool name changes", + expectErr: true, + new: &Metal3IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + }, + }, + old: &Metal3IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abcd", + }, + }, + }, + { + name: "should fail when Pool Namespace changes", + expectErr: true, + new: &Metal3IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "abc", + }, + }, + old: &Metal3IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "abcd", + }, + }, + }, + { + name: "should fail when Pool kind changes", + expectErr: true, + new: &Metal3IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Kind: "abc", + }, + }, + old: &Metal3IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Kind: "abcd", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var new, old *Metal3IPClaim + g := NewWithT(t) + new = &Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + Name: "abc-1", + }, + Spec: *tt.new, + } + + if tt.old != nil { + old = &Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + Name: "abc-1", + }, + Spec: *tt.old, + } + } else { + old = nil + } + + if tt.expectErr { + g.Expect(new.ValidateUpdate(old)).NotTo(Succeed()) + } else { + g.Expect(new.ValidateUpdate(old)).To(Succeed()) + } + }) + } +} diff --git a/api/v1alpha1/ippool_types.go b/api/v1alpha1/ippool_types.go new file mode 100644 index 00000000..e645b595 --- /dev/null +++ b/api/v1alpha1/ippool_types.go @@ -0,0 +1,115 @@ +/* +Copyright 2019 The Kubernetes 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 v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // IPPoolFinalizer allows Metal3IPPoolReconciler to clean up resources + // associated with Metal3IPPool before removing it from the apiserver. + IPPoolFinalizer = "metal3ippool.infrastructure.cluster.x-k8s.io" +) + +// MetaDataIPAddress contains the info to render th ip address. It is IP-version +// agnostic +type IPPool struct { + + // Start is the first ip address that can be rendered + Start *IPAddressStr `json:"start,omitempty"` + + // End is the last IP address that can be rendered. It is used as a validation + // that the rendered IP is in bound. + End *IPAddressStr `json:"end,omitempty"` + + // Subnet is used to validate that the rendered IP is in bounds. In case the + // Start value is not given, it is derived from the subnet ip incremented by 1 + // (`192.168.0.1` for `192.168.0.0/24`) + Subnet *IPSubnetStr `json:"subnet,omitempty"` + + // +kubebuilder:validation:Maximum=128 + // Prefix is the mask of the network as integer (max 128) + Prefix int `json:"prefix,omitempty"` + + // Gateway is the gateway ip address + Gateway *IPAddressStr `json:"gateway,omitempty"` +} + +// Metal3IPPoolSpec defines the desired state of Metal3IPPool. +type Metal3IPPoolSpec struct { + + // ClusterName is the name of the Cluster this object belongs to. + // +kubebuilder:validation:MinLength=1 + ClusterName string `json:"clusterName"` + + //Pools contains the list of IP addresses pools + Pools []IPPool `json:"pools,omitempty"` + + // PreAllocations contains the preallocated IP addresses + PreAllocations map[string]IPAddressStr `json:"preAllocations,omitempty"` + + // +kubebuilder:validation:Maximum=128 + // Prefix is the mask of the network as integer (max 128) + Prefix int `json:"prefix,omitempty"` + + // Gateway is the gateway ip address + Gateway *IPAddressStr `json:"gateway,omitempty"` + + // +kubebuilder:validation:MinLength=1 + // namePrefix is the prefix used to generate the Metal3IPAddress object names + NamePrefix string `json:"namePrefix"` +} + +// Metal3IPPoolStatus defines the observed state of Metal3IPPool. +type Metal3IPPoolStatus struct { + // LastUpdated identifies when this status was last observed. + // +optional + LastUpdated *metav1.Time `json:"lastUpdated,omitempty"` + + //Allocations contains the map of objects and IP addresses they have + Allocations map[string]IPAddressStr `json:"indexes,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:path=metal3ippools,scope=Namespaced,categories=cluster-api,shortName=m3ipp;m3ippool +// +kubebuilder:storageversion +// +kubebuilder:subresource:status +// +kubebuilder:object:root=true +// +kubebuilder:printcolumn:name="Cluster",type="string",JSONPath=".metadata.labels.cluster\\.x-k8s\\.io/cluster-name",description="Cluster to which this template belongs" + +// Metal3IPPool is the Schema for the metal3ippools API +type Metal3IPPool struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec Metal3IPPoolSpec `json:"spec,omitempty"` + Status Metal3IPPoolStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// Metal3IPPoolList contains a list of Metal3IPPool +type Metal3IPPoolList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Metal3IPPool `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Metal3IPPool{}, &Metal3IPPoolList{}) +} diff --git a/api/v1alpha1/ippool_webhook.go b/api/v1alpha1/ippool_webhook.go new file mode 100644 index 00000000..d27a92ae --- /dev/null +++ b/api/v1alpha1/ippool_webhook.go @@ -0,0 +1,84 @@ +/* +Copyright 2020 The Kubernetes 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 v1alpha1 + +import ( + "reflect" + + "github.com/pkg/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +func (c *Metal3IPPool) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(c). + Complete() +} + +// +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1alpha4-metal3ippool,mutating=false,failurePolicy=fail,groups=infrastructure.cluster.x-k8s.io,resources=metal3ippools,versions=v1alpha4,name=validation.metal3ippool.infrastructure.cluster.x-k8s.io,matchPolicy=Equivalent +// +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1alpha4-metal3ippool,mutating=true,failurePolicy=fail,groups=infrastructure.cluster.x-k8s.io,resources=metal3ippools,versions=v1alpha4,name=default.metal3ippool.infrastructure.cluster.x-k8s.io,matchPolicy=Equivalent + +var _ webhook.Defaulter = &Metal3IPPool{} +var _ webhook.Validator = &Metal3IPPool{} + +func (c *Metal3IPPool) Default() { +} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (c *Metal3IPPool) ValidateCreate() error { + return c.validate() +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (c *Metal3IPPool) ValidateUpdate(old runtime.Object) error { + allErrs := field.ErrorList{} + oldM3ipp, ok := old.(*Metal3IPPool) + if !ok || oldM3ipp == nil { + return apierrors.NewInternalError(errors.New("unable to convert existing object")) + } + + if !reflect.DeepEqual(c.Spec.NamePrefix, oldM3ipp.Spec.NamePrefix) { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "NamePrefix"), + c.Spec.NamePrefix, + "cannot be modified", + ), + ) + } + + if len(allErrs) == 0 { + return nil + } + return apierrors.NewInvalid(GroupVersion.WithKind("Metal3Data").GroupKind(), c.Name, allErrs) +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (c *Metal3IPPool) ValidateDelete() error { + return nil +} + +//No further validation for now +func (c *Metal3IPPool) validate() error { + var allErrs field.ErrorList + + if len(allErrs) == 0 { + return nil + } + return apierrors.NewInvalid(GroupVersion.WithKind("Metal3IPPool").GroupKind(), c.Name, allErrs) +} diff --git a/api/v1alpha1/ippool_webhook_test.go b/api/v1alpha1/ippool_webhook_test.go new file mode 100644 index 00000000..f58c3282 --- /dev/null +++ b/api/v1alpha1/ippool_webhook_test.go @@ -0,0 +1,133 @@ +/* +Copyright 2019 The Kubernetes 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 v1alpha1 + +import ( + "testing" + + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestMetal3IPPoolDefault(t *testing.T) { + g := NewWithT(t) + + c := &Metal3IPPool{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + }, + Spec: Metal3IPPoolSpec{}, + } + c.Default() + + g.Expect(c.Spec).To(Equal(Metal3IPPoolSpec{})) + g.Expect(c.Status).To(Equal(Metal3IPPoolStatus{})) +} + +func TestMetal3IPPoolValidation(t *testing.T) { + + tests := []struct { + name string + expectErr bool + c *Metal3IPPool + }{ + { + name: "should succeed when values and templates correct", + expectErr: false, + c: &Metal3IPPool{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + }, + Spec: Metal3IPPoolSpec{}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + if tt.expectErr { + g.Expect(tt.c.ValidateCreate()).NotTo(Succeed()) + } else { + g.Expect(tt.c.ValidateCreate()).To(Succeed()) + } + + g.Expect(tt.c.ValidateDelete()).To(Succeed()) + }) + } +} + +func TestMetal3IPPoolUpdateValidation(t *testing.T) { + + tests := []struct { + name string + expectErr bool + newPool *Metal3IPPoolSpec + oldPool *Metal3IPPoolSpec + }{ + { + name: "should succeed when values and templates correct", + expectErr: false, + newPool: &Metal3IPPoolSpec{}, + oldPool: &Metal3IPPoolSpec{}, + }, + { + name: "should fail when oldPool is nil", + expectErr: true, + newPool: &Metal3IPPoolSpec{}, + oldPool: nil, + }, + { + name: "should fail when namePrefix value changes", + expectErr: true, + newPool: &Metal3IPPoolSpec{ + NamePrefix: "abcde", + }, + oldPool: &Metal3IPPoolSpec{ + NamePrefix: "abcd", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var newPool, oldPool *Metal3IPPool + g := NewWithT(t) + newPool = &Metal3IPPool{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + }, + Spec: *tt.newPool, + } + + if tt.oldPool != nil { + oldPool = &Metal3IPPool{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + }, + Spec: *tt.oldPool, + } + } else { + oldPool = nil + } + + if tt.expectErr { + g.Expect(newPool.ValidateUpdate(oldPool)).NotTo(Succeed()) + } else { + g.Expect(newPool.ValidateUpdate(oldPool)).To(Succeed()) + } + }) + } +} diff --git a/api/v1alpha1/v1alpha4_suite_test.go b/api/v1alpha1/v1alpha4_suite_test.go new file mode 100644 index 00000000..59ccfa3f --- /dev/null +++ b/api/v1alpha1/v1alpha4_suite_test.go @@ -0,0 +1,55 @@ +/* +Copyright 2019 The Kubernetes 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 v1alpha1 + +import ( + "log" + "os" + "path/filepath" + "testing" + + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" +) + +var cfg *rest.Config +var c client.Client + +func TestMain(m *testing.M) { + t := &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + } + + err := SchemeBuilder.AddToScheme(scheme.Scheme) + if err != nil { + log.Fatal(err) + } + + if cfg, err = t.Start(); err != nil { + log.Fatal(err) + } + + if c, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}); err != nil { + log.Fatal(err) + } + + code := m.Run() + _ = t.Stop() + os.Exit(code) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000..04f2d2ad --- /dev/null +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,360 @@ +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes 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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IPPool) DeepCopyInto(out *IPPool) { + *out = *in + if in.Start != nil { + in, out := &in.Start, &out.Start + *out = new(IPAddressStr) + **out = **in + } + if in.End != nil { + in, out := &in.End, &out.End + *out = new(IPAddressStr) + **out = **in + } + if in.Subnet != nil { + in, out := &in.Subnet, &out.Subnet + *out = new(IPSubnetStr) + **out = **in + } + if in.Gateway != nil { + in, out := &in.Gateway, &out.Gateway + *out = new(IPAddressStr) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPool. +func (in *IPPool) DeepCopy() *IPPool { + if in == nil { + return nil + } + out := new(IPPool) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Metal3IPAddress) DeepCopyInto(out *Metal3IPAddress) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metal3IPAddress. +func (in *Metal3IPAddress) DeepCopy() *Metal3IPAddress { + if in == nil { + return nil + } + out := new(Metal3IPAddress) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Metal3IPAddress) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Metal3IPAddressList) DeepCopyInto(out *Metal3IPAddressList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Metal3IPAddress, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metal3IPAddressList. +func (in *Metal3IPAddressList) DeepCopy() *Metal3IPAddressList { + if in == nil { + return nil + } + out := new(Metal3IPAddressList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Metal3IPAddressList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Metal3IPAddressSpec) DeepCopyInto(out *Metal3IPAddressSpec) { + *out = *in + out.Claim = in.Claim + out.Pool = in.Pool + if in.Gateway != nil { + in, out := &in.Gateway, &out.Gateway + *out = new(IPAddressStr) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metal3IPAddressSpec. +func (in *Metal3IPAddressSpec) DeepCopy() *Metal3IPAddressSpec { + if in == nil { + return nil + } + out := new(Metal3IPAddressSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Metal3IPClaim) DeepCopyInto(out *Metal3IPClaim) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metal3IPClaim. +func (in *Metal3IPClaim) DeepCopy() *Metal3IPClaim { + if in == nil { + return nil + } + out := new(Metal3IPClaim) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Metal3IPClaim) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Metal3IPClaimList) DeepCopyInto(out *Metal3IPClaimList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Metal3IPClaim, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metal3IPClaimList. +func (in *Metal3IPClaimList) DeepCopy() *Metal3IPClaimList { + if in == nil { + return nil + } + out := new(Metal3IPClaimList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Metal3IPClaimList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Metal3IPClaimSpec) DeepCopyInto(out *Metal3IPClaimSpec) { + *out = *in + out.Pool = in.Pool +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metal3IPClaimSpec. +func (in *Metal3IPClaimSpec) DeepCopy() *Metal3IPClaimSpec { + if in == nil { + return nil + } + out := new(Metal3IPClaimSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Metal3IPClaimStatus) DeepCopyInto(out *Metal3IPClaimStatus) { + *out = *in + if in.Address != nil { + in, out := &in.Address, &out.Address + *out = new(v1.ObjectReference) + **out = **in + } + if in.ErrorMessage != nil { + in, out := &in.ErrorMessage, &out.ErrorMessage + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metal3IPClaimStatus. +func (in *Metal3IPClaimStatus) DeepCopy() *Metal3IPClaimStatus { + if in == nil { + return nil + } + out := new(Metal3IPClaimStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Metal3IPPool) DeepCopyInto(out *Metal3IPPool) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metal3IPPool. +func (in *Metal3IPPool) DeepCopy() *Metal3IPPool { + if in == nil { + return nil + } + out := new(Metal3IPPool) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Metal3IPPool) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Metal3IPPoolList) DeepCopyInto(out *Metal3IPPoolList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Metal3IPPool, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metal3IPPoolList. +func (in *Metal3IPPoolList) DeepCopy() *Metal3IPPoolList { + if in == nil { + return nil + } + out := new(Metal3IPPoolList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Metal3IPPoolList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Metal3IPPoolSpec) DeepCopyInto(out *Metal3IPPoolSpec) { + *out = *in + if in.Pools != nil { + in, out := &in.Pools, &out.Pools + *out = make([]IPPool, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.PreAllocations != nil { + in, out := &in.PreAllocations, &out.PreAllocations + *out = make(map[string]IPAddressStr, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Gateway != nil { + in, out := &in.Gateway, &out.Gateway + *out = new(IPAddressStr) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metal3IPPoolSpec. +func (in *Metal3IPPoolSpec) DeepCopy() *Metal3IPPoolSpec { + if in == nil { + return nil + } + out := new(Metal3IPPoolSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Metal3IPPoolStatus) DeepCopyInto(out *Metal3IPPoolStatus) { + *out = *in + if in.LastUpdated != nil { + in, out := &in.LastUpdated, &out.LastUpdated + *out = (*in).DeepCopy() + } + if in.Allocations != nil { + in, out := &in.Allocations, &out.Allocations + *out = make(map[string]IPAddressStr, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metal3IPPoolStatus. +func (in *Metal3IPPoolStatus) DeepCopy() *Metal3IPPoolStatus { + if in == nil { + return nil + } + out := new(Metal3IPPoolStatus) + in.DeepCopyInto(out) + return out +} diff --git a/config/certmanager/certificate.yaml b/config/certmanager/certificate.yaml new file mode 100644 index 00000000..af4e13ff --- /dev/null +++ b/config/certmanager/certificate.yaml @@ -0,0 +1,24 @@ +# The following manifests contain a self-signed issuer CR and a certificate CR. +# More document can be found at https://docs.cert-manager.io +apiVersion: cert-manager.io/v1alpha2 +kind: Issuer +metadata: + name: selfsigned-issuer + namespace: system +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1alpha2 +kind: Certificate +metadata: + name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml + namespace: system +spec: + # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize + dnsNames: + - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc + - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local + issuerRef: + kind: Issuer + name: selfsigned-issuer + secretName: $(SERVICE_NAME)-cert # this secret will not be prefixed, since it's not managed by kustomize diff --git a/config/certmanager/kustomization.yaml b/config/certmanager/kustomization.yaml new file mode 100644 index 00000000..e0182475 --- /dev/null +++ b/config/certmanager/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- certificate.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/config/certmanager/kustomizeconfig.yaml b/config/certmanager/kustomizeconfig.yaml new file mode 100644 index 00000000..28a895a4 --- /dev/null +++ b/config/certmanager/kustomizeconfig.yaml @@ -0,0 +1,19 @@ +# This configuration is for teaching kustomize how to update name ref and var substitution +nameReference: +- kind: Issuer + group: cert-manager.io + fieldSpecs: + - kind: Certificate + group: cert-manager.io + path: spec/issuerRef/name + +varReference: +- kind: Certificate + group: cert-manager.io + path: spec/commonName +- kind: Certificate + group: cert-manager.io + path: spec/dnsNames +- kind: Certificate + group: cert-manager.io + path: spec/secretName diff --git a/config/crd/bases/ipam.metal3.io_metal3ipaddresses.yaml b/config/crd/bases/ipam.metal3.io_metal3ipaddresses.yaml new file mode 100644 index 00000000..a4f0c84e --- /dev/null +++ b/config/crd/bases/ipam.metal3.io_metal3ipaddresses.yaml @@ -0,0 +1,143 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.2.8 + creationTimestamp: null + name: metal3ipaddresses.ipam.metal3.io +spec: + group: ipam.metal3.io + names: + categories: + - cluster-api + kind: Metal3IPAddress + listKind: Metal3IPAddressList + plural: metal3ipaddresses + shortNames: + - m3ipa + - m3ipaddress + - metal3ipaddress + singular: metal3ipaddress + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Metal3IPAddress is the Schema for the metal3ipaddresses API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Metal3IPAddressSpec defines the desired state of Metal3IPAddress. + properties: + address: + description: Address contains the IP address + pattern: ((^((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$)|(^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$)) + type: string + claim: + description: Claim points to the object the Metal3IPClaim was created + for. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + gateway: + description: Gateway is the gateway ip address + pattern: ((^((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$)|(^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$)) + type: string + pool: + description: Pool is the Metal3IPPool this was generated from. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + prefix: + description: Prefix is the mask of the network as integer (max 128) + maximum: 128 + type: integer + required: + - address + - claim + - pool + type: object + type: object + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/crd/bases/ipam.metal3.io_metal3ipclaims.yaml b/config/crd/bases/ipam.metal3.io_metal3ipclaims.yaml new file mode 100644 index 00000000..9cb2f7b6 --- /dev/null +++ b/config/crd/bases/ipam.metal3.io_metal3ipclaims.yaml @@ -0,0 +1,138 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.2.8 + creationTimestamp: null + name: metal3ipclaims.ipam.metal3.io +spec: + group: ipam.metal3.io + names: + categories: + - cluster-api + kind: Metal3IPClaim + listKind: Metal3IPClaimList + plural: metal3ipclaims + shortNames: + - m3ipc + - m3ipclaim + - metal3ipclaim + singular: metal3ipclaim + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Metal3IPClaim is the Schema for the metal3ipclaims API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Metal3IPClaimSpec defines the desired state of Metal3IPClaim. + properties: + pool: + description: Pool is the Metal3IPPool this was generated from. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + required: + - pool + type: object + status: + description: Metal3IPClaimStatus defines the observed state of Metal3IPClaim. + properties: + address: + description: Address is the Metal3IPAddress that was generated for + this claim. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + errorMessage: + description: ErrorMessage contains the error message + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/crd/bases/ipam.metal3.io_metal3ippools.yaml b/config/crd/bases/ipam.metal3.io_metal3ippools.yaml new file mode 100644 index 00000000..0679aa17 --- /dev/null +++ b/config/crd/bases/ipam.metal3.io_metal3ippools.yaml @@ -0,0 +1,137 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.2.8 + creationTimestamp: null + name: metal3ippools.ipam.metal3.io +spec: + group: ipam.metal3.io + names: + categories: + - cluster-api + kind: Metal3IPPool + listKind: Metal3IPPoolList + plural: metal3ippools + shortNames: + - m3ipp + - m3ippool + singular: metal3ippool + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Cluster to which this template belongs + jsonPath: .metadata.labels.cluster\.x-k8s\.io/cluster-name + name: Cluster + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: Metal3IPPool is the Schema for the metal3ippools API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Metal3IPPoolSpec defines the desired state of Metal3IPPool. + properties: + clusterName: + description: ClusterName is the name of the Cluster this object belongs + to. + minLength: 1 + type: string + gateway: + description: Gateway is the gateway ip address + pattern: ((^((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$)|(^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$)) + type: string + namePrefix: + description: namePrefix is the prefix used to generate the Metal3IPAddress + object names + minLength: 1 + type: string + pools: + description: Pools contains the list of IP addresses pools + items: + description: MetaDataIPAddress contains the info to render th ip + address. It is IP-version agnostic + properties: + end: + description: End is the last IP address that can be rendered. + It is used as a validation that the rendered IP is in bound. + pattern: ((^((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$)|(^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$)) + type: string + gateway: + description: Gateway is the gateway ip address + pattern: ((^((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$)|(^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$)) + type: string + prefix: + description: Prefix is the mask of the network as integer (max + 128) + maximum: 128 + type: integer + start: + description: Start is the first ip address that can be rendered + pattern: ((^((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$)|(^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$)) + type: string + subnet: + description: Subnet is used to validate that the rendered IP + is in bounds. In case the Start value is not given, it is + derived from the subnet ip incremented by 1 (`192.168.0.1` + for `192.168.0.0/24`) + pattern: ((^((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))/([0-9]|[1-2][0-9]|3[0-2])$)|(^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))/([0-9]|[0-9][0-9]|1[0-1][0-9]|12[0-8])$)) + type: string + type: object + type: array + preAllocations: + additionalProperties: + description: IPAddress is used for validation of an IP address + pattern: ((^((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$)|(^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$)) + type: string + description: PreAllocations contains the preallocated IP addresses + type: object + prefix: + description: Prefix is the mask of the network as integer (max 128) + maximum: 128 + type: integer + required: + - clusterName + - namePrefix + type: object + status: + description: Metal3IPPoolStatus defines the observed state of Metal3IPPool. + properties: + indexes: + additionalProperties: + description: IPAddress is used for validation of an IP address + pattern: ((^((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$)|(^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$)) + type: string + description: Allocations contains the map of objects and IP addresses + they have + type: object + lastUpdated: + description: LastUpdated identifies when this status was last observed. + format: date-time + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml new file mode 100644 index 00000000..bba9f9b4 --- /dev/null +++ b/config/crd/kustomization.yaml @@ -0,0 +1,52 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +# This kustomization.yaml is not intended to be run by itself, +# since it depends on service name and namespace that are out of this kustomize package. +# It should be run by config/default + +commonLabels: + cluster.x-k8s.io/v1alpha2: v1alpha2 + cluster.x-k8s.io/v1alpha3: v1alpha3_v1alpha4 + +resources: +- bases/infrastructure.cluster.x-k8s.io_metal3clusters.yaml +- bases/infrastructure.cluster.x-k8s.io_metal3machines.yaml +- bases/infrastructure.cluster.x-k8s.io_metal3machinetemplates.yaml +- bases/infrastructure.cluster.x-k8s.io_metal3datatemplates.yaml +- bases/infrastructure.cluster.x-k8s.io_metal3datas.yaml +- bases/infrastructure.cluster.x-k8s.io_metal3dataclaims.yaml +- bases/infrastructure.cluster.x-k8s.io_metal3ippools.yaml +- bases/infrastructure.cluster.x-k8s.io_metal3ipaddresses.yaml +- bases/infrastructure.cluster.x-k8s.io_metal3ipclaims.yaml +# +kubebuilder:scaffold:crdkustomizeresource + +patchesStrategicMerge: +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. +# patches here are for enabling the conversion webhook for each CRD +- patches/webhook_in_metal3machines.yaml +- patches/webhook_in_metal3clusters.yaml +- patches/webhook_in_metal3machinetemplates.yaml +- patches/webhook_in_metal3datatemplates.yaml +- patches/webhook_in_metal3datas.yaml +- patches/webhook_in_metal3dataclaims.yaml +- patches/webhook_in_metal3ippools.yaml +- patches/webhook_in_metal3ipaddresses.yaml +- patches/webhook_in_metal3ipclaims.yaml +# +kubebuilder:scaffold:crdkustomizewebhookpatch + +# [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. +# patches here are for enabling the CA injection for each CRD +- patches/cainjection_in_metal3machines.yaml +- patches/cainjection_in_metal3clusters.yaml +- patches/cainjection_in_metal3machinetemplates.yaml +- patches/cainjection_in_metal3datatemplates.yaml +- patches/cainjection_in_metal3datas.yaml +- patches/cainjection_in_metal3dataclaims.yaml +- patches/cainjection_in_metal3ippools.yaml +- patches/cainjection_in_metal3ipaddresses.yaml +- patches/cainjection_in_metal3ipclaims.yaml +# +kubebuilder:scaffold:crdkustomizecainjectionpatch + +# the following config is for teaching kustomize how to do kustomization for CRDs. +configurations: +- kustomizeconfig.yaml diff --git a/config/crd/kustomizeconfig.yaml b/config/crd/kustomizeconfig.yaml new file mode 100644 index 00000000..8e2d8d6b --- /dev/null +++ b/config/crd/kustomizeconfig.yaml @@ -0,0 +1,17 @@ +# This file is for teaching kustomize how to substitute name and namespace reference in CRD +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: CustomResourceDefinition + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/name + +namespace: +- kind: CustomResourceDefinition + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/namespace + create: false + +varReference: +- path: metadata/annotations diff --git a/config/crd/patches/cainjection_in_metal3clusters.yaml b/config/crd/patches/cainjection_in_metal3clusters.yaml new file mode 100644 index 00000000..f6d17861 --- /dev/null +++ b/config/crd/patches/cainjection_in_metal3clusters.yaml @@ -0,0 +1,8 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: metal3clusters.infrastructure.cluster.x-k8s.io diff --git a/config/crd/patches/cainjection_in_metal3dataclaims.yaml b/config/crd/patches/cainjection_in_metal3dataclaims.yaml new file mode 100644 index 00000000..7faf0132 --- /dev/null +++ b/config/crd/patches/cainjection_in_metal3dataclaims.yaml @@ -0,0 +1,8 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: metal3dataclaims.infrastructure.cluster.x-k8s.io diff --git a/config/crd/patches/cainjection_in_metal3datas.yaml b/config/crd/patches/cainjection_in_metal3datas.yaml new file mode 100644 index 00000000..3dc0ef85 --- /dev/null +++ b/config/crd/patches/cainjection_in_metal3datas.yaml @@ -0,0 +1,8 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: metal3datas.infrastructure.cluster.x-k8s.io diff --git a/config/crd/patches/cainjection_in_metal3datatemplates.yaml b/config/crd/patches/cainjection_in_metal3datatemplates.yaml new file mode 100644 index 00000000..025377e9 --- /dev/null +++ b/config/crd/patches/cainjection_in_metal3datatemplates.yaml @@ -0,0 +1,8 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: metal3datatemplates.infrastructure.cluster.x-k8s.io diff --git a/config/crd/patches/cainjection_in_metal3ipaddresses.yaml b/config/crd/patches/cainjection_in_metal3ipaddresses.yaml new file mode 100644 index 00000000..07686077 --- /dev/null +++ b/config/crd/patches/cainjection_in_metal3ipaddresses.yaml @@ -0,0 +1,8 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: metal3ipaddresses.infrastructure.cluster.x-k8s.io diff --git a/config/crd/patches/cainjection_in_metal3ipclaims.yaml b/config/crd/patches/cainjection_in_metal3ipclaims.yaml new file mode 100644 index 00000000..ea60aa25 --- /dev/null +++ b/config/crd/patches/cainjection_in_metal3ipclaims.yaml @@ -0,0 +1,8 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: metal3ipclaims.infrastructure.cluster.x-k8s.io diff --git a/config/crd/patches/cainjection_in_metal3ippools.yaml b/config/crd/patches/cainjection_in_metal3ippools.yaml new file mode 100644 index 00000000..cd75a055 --- /dev/null +++ b/config/crd/patches/cainjection_in_metal3ippools.yaml @@ -0,0 +1,8 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: metal3ippools.infrastructure.cluster.x-k8s.io diff --git a/config/crd/patches/cainjection_in_metal3machines.yaml b/config/crd/patches/cainjection_in_metal3machines.yaml new file mode 100644 index 00000000..5e0641ce --- /dev/null +++ b/config/crd/patches/cainjection_in_metal3machines.yaml @@ -0,0 +1,8 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: metal3machines.infrastructure.cluster.x-k8s.io diff --git a/config/crd/patches/cainjection_in_metal3machinetemplates.yaml b/config/crd/patches/cainjection_in_metal3machinetemplates.yaml new file mode 100644 index 00000000..b4ebc7eb --- /dev/null +++ b/config/crd/patches/cainjection_in_metal3machinetemplates.yaml @@ -0,0 +1,8 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: metal3machinetemplates.infrastructure.cluster.x-k8s.io diff --git a/config/crd/patches/webhook_in_metal3clusters.yaml b/config/crd/patches/webhook_in_metal3clusters.yaml new file mode 100644 index 00000000..da9789dd --- /dev/null +++ b/config/crd/patches/webhook_in_metal3clusters.yaml @@ -0,0 +1,19 @@ +# The following patch enables conversion webhook for CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: metal3clusters.infrastructure.cluster.x-k8s.io +spec: + conversion: + strategy: Webhook + webhook: + conversionReviewVersions: ["v1", "v1beta1"] + clientConfig: + # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, + # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) + caBundle: Cg== + service: + namespace: system + name: webhook-service + path: /convert diff --git a/config/crd/patches/webhook_in_metal3dataclaims.yaml b/config/crd/patches/webhook_in_metal3dataclaims.yaml new file mode 100644 index 00000000..ac2e73e3 --- /dev/null +++ b/config/crd/patches/webhook_in_metal3dataclaims.yaml @@ -0,0 +1,19 @@ +# The following patch enables conversion webhook for CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: metal3dataclaims.infrastructure.cluster.x-k8s.io +spec: + conversion: + strategy: Webhook + webhook: + conversionReviewVersions: ["v1", "v1beta1"] + clientConfig: + # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, + # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) + caBundle: Cg== + service: + namespace: system + name: webhook-service + path: /convert diff --git a/config/crd/patches/webhook_in_metal3datas.yaml b/config/crd/patches/webhook_in_metal3datas.yaml new file mode 100644 index 00000000..5ecf4e89 --- /dev/null +++ b/config/crd/patches/webhook_in_metal3datas.yaml @@ -0,0 +1,19 @@ +# The following patch enables conversion webhook for CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: metal3datas.infrastructure.cluster.x-k8s.io +spec: + conversion: + strategy: Webhook + webhook: + conversionReviewVersions: ["v1", "v1beta1"] + clientConfig: + # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, + # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) + caBundle: Cg== + service: + namespace: system + name: webhook-service + path: /convert diff --git a/config/crd/patches/webhook_in_metal3datatemplates.yaml b/config/crd/patches/webhook_in_metal3datatemplates.yaml new file mode 100644 index 00000000..eb65c21d --- /dev/null +++ b/config/crd/patches/webhook_in_metal3datatemplates.yaml @@ -0,0 +1,19 @@ +# The following patch enables conversion webhook for CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: metal3datatemplates.infrastructure.cluster.x-k8s.io +spec: + conversion: + strategy: Webhook + webhook: + conversionReviewVersions: ["v1", "v1beta1"] + clientConfig: + # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, + # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) + caBundle: Cg== + service: + namespace: system + name: webhook-service + path: /convert diff --git a/config/crd/patches/webhook_in_metal3ipaddresses.yaml b/config/crd/patches/webhook_in_metal3ipaddresses.yaml new file mode 100644 index 00000000..f9c65864 --- /dev/null +++ b/config/crd/patches/webhook_in_metal3ipaddresses.yaml @@ -0,0 +1,19 @@ +# The following patch enables conversion webhook for CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: metal3ipaddresses.infrastructure.cluster.x-k8s.io +spec: + conversion: + strategy: Webhook + webhook: + conversionReviewVersions: ["v1", "v1beta1"] + clientConfig: + # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, + # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) + caBundle: Cg== + service: + namespace: system + name: webhook-service + path: /convert diff --git a/config/crd/patches/webhook_in_metal3ipclaims.yaml b/config/crd/patches/webhook_in_metal3ipclaims.yaml new file mode 100644 index 00000000..7384e03e --- /dev/null +++ b/config/crd/patches/webhook_in_metal3ipclaims.yaml @@ -0,0 +1,19 @@ +# The following patch enables conversion webhook for CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: metal3ipclaims.infrastructure.cluster.x-k8s.io +spec: + conversion: + strategy: Webhook + webhook: + conversionReviewVersions: ["v1", "v1beta1"] + clientConfig: + # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, + # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) + caBundle: Cg== + service: + namespace: system + name: webhook-service + path: /convert diff --git a/config/crd/patches/webhook_in_metal3ippools.yaml b/config/crd/patches/webhook_in_metal3ippools.yaml new file mode 100644 index 00000000..e986e8de --- /dev/null +++ b/config/crd/patches/webhook_in_metal3ippools.yaml @@ -0,0 +1,19 @@ +# The following patch enables conversion webhook for CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: metal3ippools.infrastructure.cluster.x-k8s.io +spec: + conversion: + strategy: Webhook + webhook: + conversionReviewVersions: ["v1", "v1beta1"] + clientConfig: + # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, + # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) + caBundle: Cg== + service: + namespace: system + name: webhook-service + path: /convert diff --git a/config/crd/patches/webhook_in_metal3machines.yaml b/config/crd/patches/webhook_in_metal3machines.yaml new file mode 100644 index 00000000..7097af26 --- /dev/null +++ b/config/crd/patches/webhook_in_metal3machines.yaml @@ -0,0 +1,19 @@ +# The following patch enables conversion webhook for CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: metal3machines.infrastructure.cluster.x-k8s.io +spec: + conversion: + strategy: Webhook + webhook: + conversionReviewVersions: ["v1", "v1beta1"] + clientConfig: + # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, + # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) + caBundle: Cg== + service: + namespace: system + name: webhook-service + path: /convert diff --git a/config/crd/patches/webhook_in_metal3machinetemplates.yaml b/config/crd/patches/webhook_in_metal3machinetemplates.yaml new file mode 100644 index 00000000..100c9105 --- /dev/null +++ b/config/crd/patches/webhook_in_metal3machinetemplates.yaml @@ -0,0 +1,19 @@ +# The following patch enables conversion webhook for CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: metal3machinetemplates.infrastructure.cluster.x-k8s.io +spec: + conversion: + strategy: Webhook + webhook: + conversionReviewVersions: ["v1", "v1beta1"] + clientConfig: + # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, + # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) + caBundle: Cg== + service: + namespace: system + name: webhook-service + path: /convert diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml new file mode 100644 index 00000000..b66a7ba2 --- /dev/null +++ b/config/default/kustomization.yaml @@ -0,0 +1,12 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# Adds namespace to all resources. +namespace: capm3-system + +resources: +- namespace.yaml + +bases: +- ../rbac +- ../manager diff --git a/config/default/namespace.yaml b/config/default/namespace.yaml new file mode 100644 index 00000000..1ab3a725 --- /dev/null +++ b/config/default/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: system diff --git a/config/kustomization.yaml b/config/kustomization.yaml new file mode 100644 index 00000000..a2689560 --- /dev/null +++ b/config/kustomization.yaml @@ -0,0 +1,65 @@ +namePrefix: capm3- + +commonLabels: + cluster.x-k8s.io/provider: "infrastructure-metal3" + +bases: +- crd +- webhook # Disable this if you're not using the webhook functionality. +- default + +patchesJson6902: +- target: # NOTE: This patch needs to be repeatd for EACH CustomResourceDefinition you have under crd/bases. + group: apiextensions.k8s.io + version: v1 + kind: CustomResourceDefinition + name: metal3clusters.infrastructure.cluster.x-k8s.io + path: patch_crd_webhook_namespace.yaml +- target: + group: apiextensions.k8s.io + version: v1 + kind: CustomResourceDefinition + name: metal3machines.infrastructure.cluster.x-k8s.io + path: patch_crd_webhook_namespace.yaml +- target: + group: apiextensions.k8s.io + version: v1 + kind: CustomResourceDefinition + name: metal3machinetemplates.infrastructure.cluster.x-k8s.io + path: patch_crd_webhook_namespace.yaml +- target: + group: apiextensions.k8s.io + version: v1 + kind: CustomResourceDefinition + name: metal3datatemplates.infrastructure.cluster.x-k8s.io + path: patch_crd_webhook_namespace.yaml +- target: + group: apiextensions.k8s.io + version: v1 + kind: CustomResourceDefinition + name: metal3datas.infrastructure.cluster.x-k8s.io + path: patch_crd_webhook_namespace.yaml +- target: + group: apiextensions.k8s.io + version: v1 + kind: CustomResourceDefinition + name: metal3dataclaims.infrastructure.cluster.x-k8s.io + path: patch_crd_webhook_namespace.yaml +- target: + group: apiextensions.k8s.io + version: v1 + kind: CustomResourceDefinition + name: metal3ippools.infrastructure.cluster.x-k8s.io + path: patch_crd_webhook_namespace.yaml +- target: + group: apiextensions.k8s.io + version: v1 + kind: CustomResourceDefinition + name: metal3ipaddresses.infrastructure.cluster.x-k8s.io + path: patch_crd_webhook_namespace.yaml +- target: + group: apiextensions.k8s.io + version: v1 + kind: CustomResourceDefinition + name: metal3ipclaims.infrastructure.cluster.x-k8s.io + path: patch_crd_webhook_namespace.yaml diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml new file mode 100644 index 00000000..131cb63d --- /dev/null +++ b/config/manager/kustomization.yaml @@ -0,0 +1,13 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: +- manager.yaml + +generatorOptions: + disableNameSuffixHash: true + +patchesStrategicMerge: +- manager_image_patch.yaml +- manager_pull_policy.yaml +- manager_auth_proxy_patch.yaml diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml new file mode 100644 index 00000000..fabd221f --- /dev/null +++ b/config/manager/manager.yaml @@ -0,0 +1,80 @@ +apiVersion: v1 +kind: Service +metadata: + name: controller-manager-service + namespace: system + labels: + control-plane: controller-manager + controller-tools.k8s.io: "1.0" +spec: + selector: + control-plane: controller-manager + controller-tools.k8s.io: "1.0" + ports: + - port: 443 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system + labels: + control-plane: controller-manager + controller-tools.k8s.io: "1.0" +spec: + selector: + matchLabels: + control-plane: controller-manager + controller-tools.k8s.io: "1.0" + template: + metadata: + labels: + control-plane: controller-manager + controller-tools.k8s.io: "1.0" + spec: + containers: + - command: + - /manager + image: controller:latest + imagePullPolicy: IfNotPresent + name: manager + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + resources: + limits: + cpu: 100m + memory: 30Mi + requests: + cpu: 100m + memory: 20Mi + ports: + - containerPort: 9440 + name: healthz + protocol: TCP + readinessProbe: + httpGet: + path: /readyz + port: healthz + livenessProbe: + httpGet: + path: /healthz + port: healthz + volumeMounts: + - mountPath: /tmp/cert + name: cert + readOnly: true + terminationGracePeriodSeconds: 10 + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: webhook-server-secret +--- +apiVersion: v1 +kind: Secret +metadata: + name: webhook-server-secret + namespace: system diff --git a/config/manager/manager_auth_proxy_patch.yaml b/config/manager/manager_auth_proxy_patch.yaml new file mode 100644 index 00000000..989d6988 --- /dev/null +++ b/config/manager/manager_auth_proxy_patch.yaml @@ -0,0 +1,25 @@ +# This patch inject a sidecar container which is a HTTP proxy for the controller manager, +# it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: kube-rbac-proxy + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.4.0 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=10" + ports: + - containerPort: 8443 + name: https + - name: manager + args: + - "--metrics-addr=127.0.0.1:8080" + - "--enable-leader-election" diff --git a/config/manager/manager_image_patch.yaml b/config/manager/manager_image_patch.yaml new file mode 100644 index 00000000..70fa68da --- /dev/null +++ b/config/manager/manager_image_patch.yaml @@ -0,0 +1,12 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + # Change the value of image field below to your controller image URL + - image: quay.io/metal3-io/cluster-api-provider-metal3:master + name: manager diff --git a/config/manager/manager_prometheus_metrics_patch.yaml b/config/manager/manager_prometheus_metrics_patch.yaml new file mode 100644 index 00000000..0b96c681 --- /dev/null +++ b/config/manager/manager_prometheus_metrics_patch.yaml @@ -0,0 +1,19 @@ +# This patch enables Prometheus scraping for the manager pod. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + metadata: + annotations: + prometheus.io/scrape: 'true' + spec: + containers: + # Expose the prometheus metrics on default port + - name: manager + ports: + - containerPort: 8080 + name: metrics + protocol: TCP diff --git a/config/manager/manager_pull_policy.yaml b/config/manager/manager_pull_policy.yaml new file mode 100644 index 00000000..cd7ae12c --- /dev/null +++ b/config/manager/manager_pull_policy.yaml @@ -0,0 +1,11 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager + imagePullPolicy: IfNotPresent diff --git a/config/patch_crd_webhook_namespace.yaml b/config/patch_crd_webhook_namespace.yaml new file mode 100644 index 00000000..110f3a49 --- /dev/null +++ b/config/patch_crd_webhook_namespace.yaml @@ -0,0 +1,3 @@ +- op: replace + path: "/spec/conversion/webhook/clientConfig/service/namespace" + value: capi-webhook-system diff --git a/config/rbac/auth_proxy_role.yaml b/config/rbac/auth_proxy_role.yaml new file mode 100644 index 00000000..618f5e41 --- /dev/null +++ b/config/rbac/auth_proxy_role.yaml @@ -0,0 +1,13 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: proxy-role +rules: +- apiGroups: ["authentication.k8s.io"] + resources: + - tokenreviews + verbs: ["create"] +- apiGroups: ["authorization.k8s.io"] + resources: + - subjectaccessreviews + verbs: ["create"] diff --git a/config/rbac/auth_proxy_role_binding.yaml b/config/rbac/auth_proxy_role_binding.yaml new file mode 100644 index 00000000..48ed1e4b --- /dev/null +++ b/config/rbac/auth_proxy_role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: proxy-role +subjects: +- kind: ServiceAccount + name: default + namespace: system diff --git a/config/rbac/auth_proxy_service.yaml b/config/rbac/auth_proxy_service.yaml new file mode 100644 index 00000000..443d0c20 --- /dev/null +++ b/config/rbac/auth_proxy_service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + prometheus.io/port: "8443" + prometheus.io/scheme: https + prometheus.io/scrape: "true" + labels: + control-plane: controller-manager + name: controller-metrics-service + namespace: system +spec: + ports: + - name: https + port: 8443 + targetPort: https + selector: + control-plane: controller-manager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml new file mode 100644 index 00000000..9f5f6419 --- /dev/null +++ b/config/rbac/kustomization.yaml @@ -0,0 +1,10 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- role.yaml +- role_binding.yaml +- auth_proxy_service.yaml +- auth_proxy_role.yaml +- auth_proxy_role_binding.yaml +- leader_election_role_binding.yaml +- leader_election_role.yaml diff --git a/config/rbac/leader_election_role.yaml b/config/rbac/leader_election_role.yaml new file mode 100644 index 00000000..eaa79158 --- /dev/null +++ b/config/rbac/leader_election_role.yaml @@ -0,0 +1,32 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: leader-election-role +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - configmaps/status + verbs: + - get + - update + - patch +- apiGroups: + - "" + resources: + - events + verbs: + - create diff --git a/config/rbac/leader_election_role_binding.yaml b/config/rbac/leader_election_role_binding.yaml new file mode 100644 index 00000000..eed16906 --- /dev/null +++ b/config/rbac/leader_election_role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: leader-election-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: leader-election-role +subjects: +- kind: ServiceAccount + name: default + namespace: system diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml new file mode 100644 index 00000000..0671c61d --- /dev/null +++ b/config/rbac/role.yaml @@ -0,0 +1,243 @@ + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: manager-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - cluster.x-k8s.io + resources: + - clusters + verbs: + - get + - list + - watch +- apiGroups: + - cluster.x-k8s.io + resources: + - clusters + - clusters/status + verbs: + - get + - list + - watch +- apiGroups: + - cluster.x-k8s.io + resources: + - clusters/status + verbs: + - get +- apiGroups: + - cluster.x-k8s.io + resources: + - machines + - machines/status + verbs: + - get + - list + - watch +- apiGroups: + - infrastructure.cluster.x-k8s.io + resources: + - metal3clusters + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - infrastructure.cluster.x-k8s.io + resources: + - metal3clusters/status + verbs: + - get + - patch + - update +- apiGroups: + - infrastructure.cluster.x-k8s.io + resources: + - metal3dataclaims + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - infrastructure.cluster.x-k8s.io + resources: + - metal3dataclaims/status + verbs: + - get + - patch + - update +- apiGroups: + - infrastructure.cluster.x-k8s.io + resources: + - metal3datas + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - infrastructure.cluster.x-k8s.io + resources: + - metal3datas/status + verbs: + - get + - patch + - update +- apiGroups: + - infrastructure.cluster.x-k8s.io + resources: + - metal3datatemplates + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - infrastructure.cluster.x-k8s.io + resources: + - metal3datatemplates/status + verbs: + - get + - patch + - update +- apiGroups: + - infrastructure.cluster.x-k8s.io + resources: + - metal3ipaddresses + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - infrastructure.cluster.x-k8s.io + resources: + - metal3ipaddresses/status + verbs: + - get + - patch + - update +- apiGroups: + - infrastructure.cluster.x-k8s.io + resources: + - metal3ipclaims + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - infrastructure.cluster.x-k8s.io + resources: + - metal3ipclaims/status + verbs: + - get + - patch + - update +- apiGroups: + - infrastructure.cluster.x-k8s.io + resources: + - metal3ippools + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - infrastructure.cluster.x-k8s.io + resources: + - metal3ippools/status + verbs: + - get + - patch + - update +- apiGroups: + - infrastructure.cluster.x-k8s.io + resources: + - metal3machines + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - infrastructure.cluster.x-k8s.io + resources: + - metal3machines/status + verbs: + - get + - patch + - update +- apiGroups: + - metal3.io + resources: + - baremetalhosts + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - metal3.io + resources: + - baremetalhosts/status + verbs: + - get + - patch + - update diff --git a/config/rbac/role_binding.yaml b/config/rbac/role_binding.yaml new file mode 100644 index 00000000..8f265870 --- /dev/null +++ b/config/rbac/role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: manager-role +subjects: +- kind: ServiceAccount + name: default + namespace: system diff --git a/config/webhook/kustomization.yaml b/config/webhook/kustomization.yaml new file mode 100644 index 00000000..edd5cc7a --- /dev/null +++ b/config/webhook/kustomization.yaml @@ -0,0 +1,42 @@ +namespace: capi-webhook-system + +resources: +- manifests.yaml +- service.yaml +- ../certmanager +- ../manager + +configurations: +- kustomizeconfig.yaml + +patchesStrategicMerge: +- manager_webhook_patch.yaml +- webhookcainjection_patch.yaml # Disable this value if you don't have any defaulting or validation webhook. If you don't know, you can check if the manifests.yaml file in the same directory has any contents. + +vars: +- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR + objref: + kind: Certificate + group: cert-manager.io + version: v1alpha2 + name: serving-cert # this name should match the one in certificate.yaml + fieldref: + fieldpath: metadata.namespace +- name: CERTIFICATE_NAME + objref: + kind: Certificate + group: cert-manager.io + version: v1alpha2 + name: serving-cert # this name should match the one in certificate.yaml +- name: SERVICE_NAMESPACE # namespace of the service + objref: + kind: Service + version: v1 + name: webhook-service + fieldref: + fieldpath: metadata.namespace +- name: SERVICE_NAME + objref: + kind: Service + version: v1 + name: webhook-service diff --git a/config/webhook/kustomizeconfig.yaml b/config/webhook/kustomizeconfig.yaml new file mode 100644 index 00000000..fddf0414 --- /dev/null +++ b/config/webhook/kustomizeconfig.yaml @@ -0,0 +1,27 @@ +# the following config is for teaching kustomize where to look at when substituting vars. +# It requires kustomize v2.1.0 or newer to work properly. +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + - kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + +namespace: +- kind: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true +- kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true + +varReference: +- path: metadata/annotations +- kind: Deployment + path: spec/template/spec/volumes/secret/secretName diff --git a/config/webhook/manager_webhook_patch.yaml b/config/webhook/manager_webhook_patch.yaml new file mode 100644 index 00000000..2a593037 --- /dev/null +++ b/config/webhook/manager_webhook_patch.yaml @@ -0,0 +1,26 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager + args: + - "--metrics-addr=127.0.0.1:8080" + - "--webhook-port=9443" + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: $(SERVICE_NAME)-cert diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml new file mode 100644 index 00000000..3455451e --- /dev/null +++ b/config/webhook/manifests.yaml @@ -0,0 +1,130 @@ + +--- +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: MutatingWebhookConfiguration +metadata: + creationTimestamp: null + name: mutating-webhook-configuration +webhooks: +- clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /mutate-infrastructure-cluster-x-k8s-io-v1alpha4-metal3ipaddress + failurePolicy: Fail + matchPolicy: Equivalent + name: default.metal3ipaddress.infrastructure.cluster.x-k8s.io + rules: + - apiGroups: + - infrastructure.cluster.x-k8s.io + apiVersions: + - v1alpha4 + operations: + - CREATE + - UPDATE + resources: + - metal3ipaddresses +- clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /mutate-infrastructure-cluster-x-k8s-io-v1alpha4-metal3ipclaim + failurePolicy: Fail + matchPolicy: Equivalent + name: default.metal3ipclaim.infrastructure.cluster.x-k8s.io + rules: + - apiGroups: + - infrastructure.cluster.x-k8s.io + apiVersions: + - v1alpha4 + operations: + - CREATE + - UPDATE + resources: + - metal3ipclaims +- clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /mutate-infrastructure-cluster-x-k8s-io-v1alpha4-metal3ippool + failurePolicy: Fail + matchPolicy: Equivalent + name: default.metal3ippool.infrastructure.cluster.x-k8s.io + rules: + - apiGroups: + - infrastructure.cluster.x-k8s.io + apiVersions: + - v1alpha4 + operations: + - CREATE + - UPDATE + resources: + - metal3ippools + +--- +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: ValidatingWebhookConfiguration +metadata: + creationTimestamp: null + name: validating-webhook-configuration +webhooks: +- clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /validate-infrastructure-cluster-x-k8s-io-v1alpha4-metal3ipaddress + failurePolicy: Fail + matchPolicy: Equivalent + name: validation.metal3ipaddress.infrastructure.cluster.x-k8s.io + rules: + - apiGroups: + - infrastructure.cluster.x-k8s.io + apiVersions: + - v1alpha4 + operations: + - CREATE + - UPDATE + resources: + - metal3ipaddresses +- clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /validate-infrastructure-cluster-x-k8s-io-v1alpha4-metal3ipclaim + failurePolicy: Fail + matchPolicy: Equivalent + name: validation.metal3ipclaim.infrastructure.cluster.x-k8s.io + rules: + - apiGroups: + - infrastructure.cluster.x-k8s.io + apiVersions: + - v1alpha4 + operations: + - CREATE + - UPDATE + resources: + - metal3ipclaims +- clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /validate-infrastructure-cluster-x-k8s-io-v1alpha4-metal3ippool + failurePolicy: Fail + matchPolicy: Equivalent + name: validation.metal3ippool.infrastructure.cluster.x-k8s.io + rules: + - apiGroups: + - infrastructure.cluster.x-k8s.io + apiVersions: + - v1alpha4 + operations: + - CREATE + - UPDATE + resources: + - metal3ippools diff --git a/config/webhook/service.yaml b/config/webhook/service.yaml new file mode 100644 index 00000000..9bc95014 --- /dev/null +++ b/config/webhook/service.yaml @@ -0,0 +1,10 @@ + +apiVersion: v1 +kind: Service +metadata: + name: webhook-service + namespace: system +spec: + ports: + - port: 443 + targetPort: webhook-server diff --git a/config/webhook/webhookcainjection_patch.yaml b/config/webhook/webhookcainjection_patch.yaml new file mode 100644 index 00000000..7e79bf99 --- /dev/null +++ b/config/webhook/webhookcainjection_patch.yaml @@ -0,0 +1,15 @@ +# This patch add annotation to admission webhook config and +# the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: MutatingWebhookConfiguration +metadata: + name: mutating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) +--- +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: ValidatingWebhookConfiguration +metadata: + name: validating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) diff --git a/controllers/metal3ippool_controller.go b/controllers/metal3ippool_controller.go new file mode 100644 index 00000000..a63d88e6 --- /dev/null +++ b/controllers/metal3ippool_controller.go @@ -0,0 +1,221 @@ +/* +Copyright 2019 The Kubernetes 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 controllers + +import ( + "context" + "time" + + "github.com/go-logr/logr" + ipamv1 "github.com/metal3-io/ipam/api/v1alpha1" + "github.com/metal3-io/ipam/ipam" + "github.com/pkg/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + capi "sigs.k8s.io/cluster-api/api/v1alpha3" + "sigs.k8s.io/cluster-api/util" + "sigs.k8s.io/cluster-api/util/patch" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +const ( + ipPoolControllerName = "Metal3IPPool-controller" + requeueAfter = time.Second * 30 +) + +// Metal3IPPoolReconciler reconciles a Metal3IPPool object +type Metal3IPPoolReconciler struct { + Client client.Client + ManagerFactory ipam.ManagerFactoryInterface + Log logr.Logger +} + +// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=metal3ippools,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=metal3ippools/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=metal3ipclaims,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=metal3ipclaims/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=metal3ipaddresses,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=metal3ipaddresses/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters,verbs=get;list;watch +// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters/status,verbs=get +// +kubebuilder:rbac:groups="",resources=events,verbs=get;list;watch;create;update;patch +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete + +// Reconcile handles Metal3Machine events +func (r *Metal3IPPoolReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, rerr error) { + ctx := context.Background() + metadataLog := r.Log.WithName(ipPoolControllerName).WithValues("metal3-ippool", req.NamespacedName) + + // Fetch the Metal3IPPool instance. + ipamv1IPPool := &ipamv1.Metal3IPPool{} + + if err := r.Client.Get(ctx, req.NamespacedName, ipamv1IPPool); err != nil { + if apierrors.IsNotFound(err) { + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + helper, err := patch.NewHelper(ipamv1IPPool, r.Client) + if err != nil { + return ctrl.Result{}, errors.Wrap(err, "failed to init patch helper") + } + // Always patch ipamv1IPPool exiting this function so we can persist any Metal3IPPool changes. + defer func() { + err := helper.Patch(ctx, ipamv1IPPool) + if err != nil { + metadataLog.Info("failed to Patch ipamv1IPPool") + } + }() + + cluster := &capi.Cluster{} + key := client.ObjectKey{ + Name: ipamv1IPPool.Spec.ClusterName, + Namespace: ipamv1IPPool.Namespace, + } + + if ipamv1IPPool.ObjectMeta.Labels == nil { + ipamv1IPPool.ObjectMeta.Labels = make(map[string]string) + } + ipamv1IPPool.ObjectMeta.Labels[capi.ClusterLabelName] = ipamv1IPPool.Spec.ClusterName + ipamv1IPPool.ObjectMeta.Labels[capi.ProviderLabelName] = "infrastructure-metal3" + + // Fetch the Cluster. Ignore an error if the deletion timestamp is set + err = r.Client.Get(ctx, key, cluster) + if ipamv1IPPool.ObjectMeta.DeletionTimestamp.IsZero() { + if err != nil { + metadataLog.Info("Error fetching cluster. It might not exist yet, Requeuing") + return ctrl.Result{}, nil + } + } + + // Create a helper for managing the metadata object. + ipPoolMgr, err := r.ManagerFactory.NewIPPoolManager(ipamv1IPPool, metadataLog) + if err != nil { + return ctrl.Result{}, errors.Wrapf(err, "failed to create helper for managing the IP pool") + } + + metadataLog = metadataLog.WithValues("cluster", cluster.Name) + if err := ipPoolMgr.SetClusterOwnerRef(cluster); err != nil { + return ctrl.Result{}, err + } + + // Return early if the Metadata or Cluster is paused. + if util.IsPaused(cluster, ipamv1IPPool) { + metadataLog.Info("reconciliation is paused for this object") + return ctrl.Result{Requeue: true, RequeueAfter: requeueAfter}, nil + } + + // Handle deleted metadata + if !ipamv1IPPool.ObjectMeta.DeletionTimestamp.IsZero() { + return r.reconcileDelete(ctx, ipPoolMgr) + } + + // Handle non-deleted machines + return r.reconcileNormal(ctx, ipPoolMgr) +} + +func (r *Metal3IPPoolReconciler) reconcileNormal(ctx context.Context, + ipPoolMgr ipam.IPPoolManagerInterface, +) (ctrl.Result, error) { + // If the Metal3IPPool doesn't have finalizer, add it. + ipPoolMgr.SetFinalizer() + + _, err := ipPoolMgr.UpdateAddresses(ctx) + if err != nil { + return checkRequeueError(err, "Failed to create the missing data") + } + + return ctrl.Result{}, nil +} + +func (r *Metal3IPPoolReconciler) reconcileDelete(ctx context.Context, + ipPoolMgr ipam.IPPoolManagerInterface, +) (ctrl.Result, error) { + + allocationsNb, err := ipPoolMgr.UpdateAddresses(ctx) + if err != nil { + return checkRequeueError(err, "Failed to delete the old secrets") + } + + if allocationsNb == 0 { + // metal3ippool is marked for deletion and ready to be deleted, + // so remove the finalizer. + ipPoolMgr.UnsetFinalizer() + } + + return ctrl.Result{}, nil +} + +// SetupWithManager will add watches for this controller +func (r *Metal3IPPoolReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&ipamv1.Metal3IPPool{}). + Watches( + &source.Kind{Type: &ipamv1.Metal3IPClaim{}}, + &handler.EnqueueRequestsFromMapFunc{ + ToRequests: handler.ToRequestsFunc(r.Metal3IPClaimToMetal3IPPool), + }, + // Do not trigger a reconciliation on updates of the claim, as the Spec + // fields are immutable + builder.WithPredicates(predicate.Funcs{ + UpdateFunc: func(e event.UpdateEvent) bool { return false }, + CreateFunc: func(e event.CreateEvent) bool { return true }, + DeleteFunc: func(e event.DeleteEvent) bool { return true }, + GenericFunc: func(e event.GenericEvent) bool { return false }, + }), + ). + Complete(r) +} + +// Metal3IPClaimToMetal3IPPool will return a reconcile request for a +// Metal3DataTemplate if the event is for a +// Metal3IPClaim and that Metal3IPClaim references a Metal3DataTemplate +func (r *Metal3IPPoolReconciler) Metal3IPClaimToMetal3IPPool(obj handler.MapObject) []ctrl.Request { + if m3ipc, ok := obj.Object.(*ipamv1.Metal3IPClaim); ok { + if m3ipc.Spec.Pool.Name != "" { + namespace := m3ipc.Spec.Pool.Namespace + if namespace == "" { + namespace = m3ipc.Namespace + } + return []ctrl.Request{ + ctrl.Request{ + NamespacedName: types.NamespacedName{ + Name: m3ipc.Spec.Pool.Name, + Namespace: namespace, + }, + }, + } + } + } + return []ctrl.Request{} +} + +func checkRequeueError(err error, errMessage string) (ctrl.Result, error) { + if err == nil { + return ctrl.Result{}, nil + } + if requeueErr, ok := errors.Cause(err).(ipam.HasRequeueAfterError); ok { + return ctrl.Result{Requeue: true, RequeueAfter: requeueErr.GetRequeueAfter()}, nil + } + return ctrl.Result{}, errors.Wrap(err, errMessage) +} diff --git a/controllers/metal3ippool_controller_test.go b/controllers/metal3ippool_controller_test.go new file mode 100644 index 00000000..bd068cd2 --- /dev/null +++ b/controllers/metal3ippool_controller_test.go @@ -0,0 +1,407 @@ +/* +Copyright 2019 The Kubernetes 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 controllers + +import ( + "context" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" + + "github.com/golang/mock/gomock" + ipamv1 "github.com/metal3-io/ipam/api/v1alpha1" + "github.com/metal3-io/ipam/ipam" + ipam_mocks "github.com/metal3-io/ipam/ipam/mocks" + "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/klog/klogr" + capi "sigs.k8s.io/cluster-api/api/v1alpha3" + // ctrl "sigs.k8s.io/controller-runtime" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +var ( + testObjectMeta = metav1.ObjectMeta{ + Name: "abc", + Namespace: "myns", + } +) + +var _ = Describe("Metal3IPPool controller", func() { + + type testCaseReconcile struct { + expectError bool + expectRequeue bool + expectManager bool + m3ipp *ipamv1.Metal3IPPool + cluster *capi.Cluster + managerError bool + reconcileNormal bool + reconcileNormalError bool + reconcileDeleteError bool + setOwnerRefError bool + } + + DescribeTable("Test Reconcile", + func(tc testCaseReconcile) { + gomockCtrl := gomock.NewController(GinkgoT()) + f := ipam_mocks.NewMockManagerFactoryInterface(gomockCtrl) + m := ipam_mocks.NewMockIPPoolManagerInterface(gomockCtrl) + + objects := []runtime.Object{} + if tc.m3ipp != nil { + objects = append(objects, tc.m3ipp) + } + if tc.cluster != nil { + objects = append(objects, tc.cluster) + } + c := fake.NewFakeClientWithScheme(setupScheme(), objects...) + + if tc.managerError { + f.EXPECT().NewIPPoolManager(gomock.Any(), gomock.Any()).Return(nil, errors.New("")) + } else if tc.expectManager { + f.EXPECT().NewIPPoolManager(gomock.Any(), gomock.Any()).Return(m, nil) + } + + if tc.expectManager { + if tc.setOwnerRefError { + m.EXPECT().SetClusterOwnerRef(gomock.Any()).Return(errors.New("")) + } else { + m.EXPECT().SetClusterOwnerRef(gomock.Any()).Return(nil) + } + } + + if tc.m3ipp != nil && !tc.m3ipp.DeletionTimestamp.IsZero() && tc.reconcileDeleteError { + m.EXPECT().UpdateAddresses(context.TODO()).Return(0, errors.New("")) + } else if tc.m3ipp != nil && !tc.m3ipp.DeletionTimestamp.IsZero() { + m.EXPECT().UpdateAddresses(context.TODO()).Return(0, nil) + m.EXPECT().UnsetFinalizer() + } + + if tc.m3ipp != nil && tc.m3ipp.DeletionTimestamp.IsZero() && + tc.reconcileNormal { + m.EXPECT().SetFinalizer() + if tc.reconcileNormalError { + m.EXPECT().UpdateAddresses(context.TODO()).Return(0, errors.New("")) + } else { + m.EXPECT().UpdateAddresses(context.TODO()).Return(1, nil) + } + } + + ipPoolReconcile := &Metal3IPPoolReconciler{ + Client: c, + ManagerFactory: f, + Log: klogr.New(), + } + + req := reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: "abc", + Namespace: "myns", + }, + } + + result, err := ipPoolReconcile.Reconcile(req) + + if tc.expectError || tc.managerError || tc.reconcileNormalError { + Expect(err).To(HaveOccurred()) + } else { + Expect(err).NotTo(HaveOccurred()) + } + if tc.expectRequeue { + Expect(result.Requeue).To(BeTrue()) + } else { + Expect(result.Requeue).To(BeFalse()) + } + gomockCtrl.Finish() + }, + Entry("Metal3IPPool not found", testCaseReconcile{}), + Entry("Cluster not found", testCaseReconcile{ + m3ipp: &ipamv1.Metal3IPPool{ + ObjectMeta: testObjectMeta, + Spec: ipamv1.Metal3IPPoolSpec{ClusterName: "abc"}, + }, + }), + Entry("Deletion, Cluster not found", testCaseReconcile{ + m3ipp: &ipamv1.Metal3IPPool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + Namespace: "myns", + DeletionTimestamp: ×tampNow, + }, + Spec: ipamv1.Metal3IPPoolSpec{ClusterName: "abc"}, + }, + expectManager: true, + }), + Entry("Deletion, Cluster not found, error", testCaseReconcile{ + m3ipp: &ipamv1.Metal3IPPool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + Namespace: "myns", + DeletionTimestamp: ×tampNow, + }, + Spec: ipamv1.Metal3IPPoolSpec{ClusterName: "abc"}, + }, + expectManager: true, + reconcileDeleteError: true, + expectError: true, + }), + Entry("Paused cluster", testCaseReconcile{ + m3ipp: &ipamv1.Metal3IPPool{ + ObjectMeta: testObjectMeta, + Spec: ipamv1.Metal3IPPoolSpec{ClusterName: "abc"}, + }, + cluster: &capi.Cluster{ + ObjectMeta: testObjectMeta, + Spec: capi.ClusterSpec{ + Paused: true, + }, + }, + expectRequeue: true, + expectManager: true, + }), + Entry("Error in manager", testCaseReconcile{ + m3ipp: &ipamv1.Metal3IPPool{ + ObjectMeta: testObjectMeta, + Spec: ipamv1.Metal3IPPoolSpec{ClusterName: "abc"}, + }, + cluster: &capi.Cluster{ + ObjectMeta: testObjectMeta, + }, + managerError: true, + }), + Entry("Reconcile normal error", testCaseReconcile{ + m3ipp: &ipamv1.Metal3IPPool{ + ObjectMeta: testObjectMeta, + Spec: ipamv1.Metal3IPPoolSpec{ClusterName: "abc"}, + }, + cluster: &capi.Cluster{ + ObjectMeta: testObjectMeta, + }, + reconcileNormal: true, + reconcileNormalError: true, + expectManager: true, + }), + Entry("Reconcile normal no cluster", testCaseReconcile{ + m3ipp: &ipamv1.Metal3IPPool{ + ObjectMeta: testObjectMeta, + Spec: ipamv1.Metal3IPPoolSpec{ClusterName: "abc"}, + }, + reconcileNormal: false, + expectManager: false, + }), + Entry("Reconcile normal no error", testCaseReconcile{ + m3ipp: &ipamv1.Metal3IPPool{ + ObjectMeta: testObjectMeta, + Spec: ipamv1.Metal3IPPoolSpec{ClusterName: "abc"}, + }, + cluster: &capi.Cluster{ + ObjectMeta: testObjectMeta, + }, + reconcileNormal: true, + expectManager: true, + }), + ) + + type reconcileNormalTestCase struct { + ExpectError bool + ExpectRequeue bool + UpdateError bool + } + + DescribeTable("ReconcileNormal tests", + func(tc reconcileNormalTestCase) { + gomockCtrl := gomock.NewController(GinkgoT()) + + c := fake.NewFakeClientWithScheme(setupScheme()) + + ipPoolReconcile := &Metal3IPPoolReconciler{ + Client: c, + ManagerFactory: ipam.NewManagerFactory(c), + Log: klogr.New(), + } + m := ipam_mocks.NewMockIPPoolManagerInterface(gomockCtrl) + + m.EXPECT().SetFinalizer() + + if !tc.UpdateError { + m.EXPECT().UpdateAddresses(context.TODO()).Return(1, nil) + } else { + m.EXPECT().UpdateAddresses(context.TODO()).Return(0, errors.New("")) + } + + res, err := ipPoolReconcile.reconcileNormal(context.TODO(), m) + gomockCtrl.Finish() + + if tc.ExpectError { + Expect(err).To(HaveOccurred()) + } else { + Expect(err).NotTo(HaveOccurred()) + } + if tc.ExpectRequeue { + Expect(res.Requeue).To(BeTrue()) + } else { + Expect(res.Requeue).To(BeFalse()) + } + }, + Entry("No error", reconcileNormalTestCase{ + ExpectError: false, + ExpectRequeue: false, + }), + Entry("Update error", reconcileNormalTestCase{ + UpdateError: true, + ExpectError: true, + ExpectRequeue: false, + }), + ) + + type reconcileDeleteTestCase struct { + ExpectError bool + ExpectRequeue bool + DeleteReady bool + DeleteError bool + } + + DescribeTable("ReconcileDelete tests", + func(tc reconcileDeleteTestCase) { + gomockCtrl := gomock.NewController(GinkgoT()) + + c := fake.NewFakeClientWithScheme(setupScheme()) + + ipPoolReconcile := &Metal3IPPoolReconciler{ + Client: c, + ManagerFactory: ipam.NewManagerFactory(c), + Log: klogr.New(), + } + m := ipam_mocks.NewMockIPPoolManagerInterface(gomockCtrl) + + if !tc.DeleteError && tc.DeleteReady { + m.EXPECT().UpdateAddresses(context.TODO()).Return(0, nil) + m.EXPECT().UnsetFinalizer() + } else if !tc.DeleteError { + m.EXPECT().UpdateAddresses(context.TODO()).Return(1, nil) + } else { + m.EXPECT().UpdateAddresses(context.TODO()).Return(0, errors.New("")) + } + + res, err := ipPoolReconcile.reconcileDelete(context.TODO(), m) + gomockCtrl.Finish() + + if tc.ExpectError { + Expect(err).To(HaveOccurred()) + } else { + Expect(err).NotTo(HaveOccurred()) + } + if tc.ExpectRequeue { + Expect(res.Requeue).To(BeTrue()) + } else { + Expect(res.Requeue).To(BeFalse()) + } + + }, + Entry("No error", reconcileDeleteTestCase{ + ExpectError: false, + ExpectRequeue: false, + }), + Entry("Delete error", reconcileDeleteTestCase{ + DeleteError: true, + ExpectError: true, + ExpectRequeue: false, + }), + Entry("Delete ready", reconcileDeleteTestCase{ + ExpectError: false, + ExpectRequeue: false, + DeleteReady: true, + }), + ) + + type TestCaseM3IPCToM3IPP struct { + IPClaim *ipamv1.Metal3IPClaim + ExpectRequest bool + } + + DescribeTable("Metal3IPClaim To Metal3IPPool tests", + func(tc TestCaseM3IPCToM3IPP) { + r := Metal3IPPoolReconciler{} + obj := handler.MapObject{ + Object: tc.IPClaim, + } + reqs := r.Metal3IPClaimToMetal3IPPool(obj) + + if tc.ExpectRequest { + Expect(len(reqs)).To(Equal(1), "Expected 1 request, found %d", len(reqs)) + + req := reqs[0] + Expect(req.NamespacedName.Name).To(Equal(tc.IPClaim.Spec.Pool.Name), + "Expected name %s, found %s", tc.IPClaim.Spec.Pool.Name, req.NamespacedName.Name) + if tc.IPClaim.Spec.Pool.Namespace == "" { + Expect(req.NamespacedName.Namespace).To(Equal(tc.IPClaim.Namespace), + "Expected namespace %s, found %s", tc.IPClaim.Namespace, req.NamespacedName.Namespace) + } else { + Expect(req.NamespacedName.Namespace).To(Equal(tc.IPClaim.Spec.Pool.Namespace), + "Expected namespace %s, found %s", tc.IPClaim.Spec.Pool.Namespace, req.NamespacedName.Namespace) + } + + } else { + Expect(len(reqs)).To(Equal(0), "Expected 0 request, found %d", len(reqs)) + + } + }, + Entry("No Metal3IPPool in Spec", + TestCaseM3IPCToM3IPP{ + IPClaim: &ipamv1.Metal3IPClaim{ + ObjectMeta: testObjectMeta, + Spec: ipamv1.Metal3IPClaimSpec{}, + }, + ExpectRequest: false, + }, + ), + Entry("Metal3IPPool in Spec, with namespace", + TestCaseM3IPCToM3IPP{ + IPClaim: &ipamv1.Metal3IPClaim{ + ObjectMeta: testObjectMeta, + Spec: ipamv1.Metal3IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + }, + }, + ExpectRequest: true, + }, + ), + Entry("Metal3IPPool in Spec, no namespace", + TestCaseM3IPCToM3IPP{ + IPClaim: &ipamv1.Metal3IPClaim{ + ObjectMeta: testObjectMeta, + Spec: ipamv1.Metal3IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + }, + }, + }, + ExpectRequest: true, + }, + ), + ) +}) diff --git a/controllers/suite_test.go b/controllers/suite_test.go new file mode 100644 index 00000000..104c65da --- /dev/null +++ b/controllers/suite_test.go @@ -0,0 +1,136 @@ +/* + +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 controllers + +import ( + "path/filepath" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/klog" + "k8s.io/klog/klogr" + + ipamv1 "github.com/metal3-io/ipam/api/v1alpha1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + "sigs.k8s.io/controller-runtime/pkg/envtest/printer" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + // +kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment +var timestampNow = metav1.Now() + +const ( + namespaceName = "testNameSpace" +) + +func init() { + klog.InitFlags(nil) + logf.SetLogger(klogr.New()) + + // Register required object kinds with global scheme. + _ = apiextensionsv1.AddToScheme(scheme.Scheme) + _ = clusterv1.AddToScheme(scheme.Scheme) + _ = ipamv1.AddToScheme(scheme.Scheme) +} + +func setupScheme() *runtime.Scheme { + s := runtime.NewScheme() + + if err := clusterv1.AddToScheme(s); err != nil { + panic(err) + } + + if err := ipamv1.AddToScheme(s); err != nil { + panic(err) + } + + return s +} +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecsWithDefaultAndCustomReporters(t, + "Controller Suite", + []Reporter{printer.NewlineReporter{}}) +} + +var _ = BeforeSuite(func(done Done) { + logf.SetLogger(zap.LoggerTo(GinkgoWriter, true)) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, + } + + var err error + cfg, err = testEnv.Start() + Expect(err).ToNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + + err = ipamv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + err = apiextensionsv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).ToNot(HaveOccurred()) + Expect(k8sClient).ToNot(BeNil()) + + close(done) +}, 60) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).ToNot(HaveOccurred()) +}) + +var deletionTimestamp = metav1.Now() + +func contains(haystack []string, needle string) bool { + for _, straw := range haystack { + if straw == needle { + return true + } + } + return false +} + +func getKey(objectName string) *client.ObjectKey { + return &client.ObjectKey{ + Name: objectName, + Namespace: namespaceName, + } +} diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 00000000..a4da8c03 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,928 @@ +# API and Resource Definitions + +This describes a setup where the following Cluster API core components and +Metal3-io components are deployed : + +* Cluster API manager +* Cluster API Bootstrap Provider Kubeadm (CABPK) manager +* Cluster API Kubeadm Control Plane manager +* Baremetal Operator (including the Ironic setup) +* Cluster API Provider Metal3 (CAPM3) + +## BareMetalHost + +The BareMetalHost is an object from +[baremetal-operator](https://github.com/metal3-io/baremetal-operator). Each +CR represents a physical host with BMC credentials, hardware status etc. + +BareMetalHost exposes those different fields that are secret references: + +* **userData** : for a cloud-init user-data in a secret with the key `userData` +* **metaData** : for a cloud-init metadata in a secret with the key `metaData` +* **networkData** : for a cloud-init network data in a secret with the key + `networkData` + +For the metaData, soome values are set by default to maintain compatibility: + +* **uuid**: This is the BareMetalHost UID +* **metal3-namespace**: the name of the BareMetalHost +* **metal3-name**: The name of the BareMetalHost +* **local-hostname**: The name of the BareMetalHost +* **local_hostname**: The namespace of the BareMetalHost + +However, setting any of those values in the metaData secret will override those +default values. + +## Cluster + +A Cluster is a Cluster API core object representing a Kubernetes cluster. + +Example cluster: + +```yaml +apiVersion: cluster.x-k8s.io/v1alpha3 +kind: Cluster +metadata: + name: cluster +spec: + clusterNetwork: + services: + cidrBlocks: ["10.96.0.0/12"] + pods: + cidrBlocks: ["192.168.0.0/18"] + serviceDomain: "cluster.local" + infrastructureRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 + kind: Metal3Cluster + name: m3cluster + controlPlaneRef: + kind: KubeadmControlPlane + apiVersion: controlplane.cluster.x-k8s.io/v1alpha3 + name: m3cluster-controlplane +``` + +## Metal3Cluster + +The metal3Cluster object contains information related to the deployment of +the cluster on Baremetal. It currently has two specification fields : + +* **controlPlaneEndpoint**: contains the target cluster API server address and + port +* **noCloudProvider**: (true/false) Whether the cluster will not be deployed + with an external cloud provider. If set to true, CAPM3 will patch the target + cluster node objects to add a providerID. This will allow the CAPI process to + continue even if the cluster is deployed without cloud provider. + +Example metal3cluster : + +```yaml +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 +kind: Metal3Cluster +metadata: + name: m3cluster +spec: + controlPlaneEndpoint: + host: 192.168.111.249 + port: 6443 + noCloudProvider: true +``` + +## KubeadmControlPlane + +This object contains all information related to the control plane configuration. +It references an **infrastructureTemplate** that must be a +*Metal3MachineTemplate* in this case. + +For example: + +```yaml +kind: KubeadmControlPlane +apiVersion: controlplane.cluster.x-k8s.io/v1alpha3 +metadata: + name: m3cluster-controlplane +spec: + replicas: 3 + version: v1.17.0 + infrastructureTemplate: + kind: Metal3MachineTemplate + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 + name: m3cluster-controlplane + kubeadmConfigSpec: + initConfiguration: + nodeRegistration: + name: 'host0' + kubeletExtraArgs: + cloud-provider: baremetal + clusterConfiguration: + apiServer: + extraArgs: + cloud-provider: baremetal + controllerManager: + extraArgs: + cloud-provider: baremetal + joinConfiguration: + controlPlane: {} + nodeRegistration: + name: 'host0' + kubeletExtraArgs: + cloud-provider: baremetal +``` + +## KubeadmConfig + +The KubeadmConfig object is for CABPK. It contains the node Kubeadm +configuration and additional commands to run on the node for the setup. + +In order to deploy Kubernetes successfully, you need to know the cluster API +address before deployment. However, if you are deploying an HA cluster or if you +are deploying without using static ip addresses, the cluster API server address +is unknown. A solution to go around the problem is to deploy Keepalived. +Keepalived allows you to set up a virtual IP, defined beforehand, and shared +by the nodes. Hence the commands to set up Keepalived have to run before +kubeadm. + +The content of a KubeadmConfig can contain Jinja2 template elements, since the +cloud-init renders the cloud-config as a Jinja2 template. It is possible to +use metadata from cloud-init, using the following: `{{ ds.meta_data.}}`. +The keys and values are passed to cloud-init through a `Metal3DataTemplate` +object (see below). + +Example KubeadmConfig: + +```yaml +apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3 +kind: KubeadmConfig +metadata: + name: controlplane-0 +spec: + initConfiguration: + nodeRegistration: + name: '{{ ds.meta_data.name }}' + kubeletExtraArgs: + node-labels: 'metal3.io/uuid={{ ds.meta_data.uuid }}' + preKubeadmCommands: + - apt update -y + - apt install net-tools -y + - apt install -y gcc linux-headers-$(uname -r) + - apt install -y keepalived + - systemctl start keepalived + - systemctl enable keepalived + - >- + apt install apt-transport-https ca-certificates curl gnupg-agent + software-properties-common -y + - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - + - >- + add-apt-repository "deb [arch=amd64] + https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" + - apt update -y + - apt install docker-ce docker-ce-cli containerd.io -y + - usermod -aG docker ubuntu + - >- + curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg + | apt-key add - + - >- + echo 'deb https://apt.kubernetes.io/ kubernetes-xenial main' + > /etc/apt/sources.list.d/kubernetes.list + - apt update + - apt install -y kubelet kubeadm kubectl + - systemctl enable --now kubelet + postKubeadmCommands: + - mkdir -p /home/ubuntu/.kube + - cp /etc/kubernetes/admin.conf /home/ubuntu/.kube/config + - chown ubuntu:ubuntu /home/ubuntu/.kube/config + files: + - path: /etc/keepalived/keepalived.conf + content: | + ! Configuration File for keepalived + global_defs { + notification_email { + sysadmin@example.com + support@example.com + } + notification_email_from lb@example.com + smtp_server localhost + smtp_connect_timeout 30 + } + vrrp_instance VI_1 { + state MASTER + interface enp2s0 + virtual_router_id 1 + priority 101 + advert_int 1 + virtual_ipaddress { + 192.168.111.249 + } + } +``` + +## Machine + +A Machine is a Cluster API core object representing a Kubernetes node. A machine +has a reference to a KubeadmConfig and a reference to a metal3machine. + +Example Machine: + +```yaml +apiVersion: cluster.x-k8s.io/v1alpha3 +kind: Machine +metadata: + name: controlplane-0 + labels: + cluster.x-k8s.io/control-plane: "true" + cluster.x-k8s.io/cluster-name: "cluster" +spec: + version: 1.16 + bootstrap: + configRef: + apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3 + kind: KubeadmConfig + name: controlplane-0 + infrastructureRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 + kind: Metal3Machine + name: controlplane-0 +``` + +## Metal3Machine + +The Metal3Machine contains information related to the deployment of the +BareMetalHost such as the image and the host selector. For each machine, there +must be a Metal3Machine. + +The fields are : + +* **image** -- This includes two sub-fields, `url` and `checksum`, which + include the URL to the image and the URL to a checksum for that image. These + fields are required. The image will be used for provisioning of the + `BareMetalHost` chosen by the `Machine` actuator. + +* **userData** -- This includes two sub-fields, `name` and `namespace`, which + reference a `Secret` that contains base64 encoded user-data to be written to + a config drive on the provisioned `BareMetalHost`. This field is optional and + is automatically set by CAPM3 with the userData from the machine object. If + you want to overwrite the userData, this should be done in the CAPI machine. + +* **dataTemplate** -- This includes a reference to a Metal3DataTemplate object + containing the metadata and network data templates, and includes two fields, + `name` and `namespace`. + +* **metaData** is a reference to a secret containing the metadata rendered from + the Metal3DataTemplate metadata template object automatically. In case this + would not be managed by the Metal3DataTemplate controller, if provided by the + user for example, the ownerreference should be set properly to ensure that the + secret belongs to the cluster ownerReference tree (see + [doc](https://cluster-api.sigs.k8s.io/clusterctl/provider-contract.html#ownerreferences-chain)). + +* **networkData** is a reference to a secret containing the network data + rendered from the Metal3DataTemplate metadata template object automatically. + In case this would not be managed by the Metal3DataTemplate controller, if + provided by the user for example, the ownerreference should be set properly to + ensure that the secret belongs to the cluster ownerReference tree (see + [doc](https://cluster-api.sigs.k8s.io/clusterctl/provider-contract.html#ownerreferences-chain)). + The content of the secret should be a yaml equivalent of a json object that + follows the format definition that can be found + [here](https://docs.openstack.org/nova/latest/_downloads/9119ca7ac90aa2990e762c08baea3a36/network_data.json). + +* **hostSelector** -- Specify criteria for matching labels on `BareMetalHost` + objects. This can be used to limit the set of available `BareMetalHost` + objects chosen for this `Machine`. + +The `metaData` and `networkData` field in the `spec` section are for the user +to give directly a secret to use as metaData or networkData. The `userData`, +`metaData` and `networkData` fields in the `status` section are for the +controller to store the reference to the secret that is actually being used, +whether it is from one of the spec fields, or somehow generated. This is aimed +at making a clear difference between the desired state from the user (whether +it is with a DataTemplate reference, or direct `metaData` or `userData` secrets) +and what the controller is actually using. + +The `dataTemplate` field consists of an object reference to a +Metal3DataTemplate object containing the templates for the metadata and +network data generation for this Metal3Machine. The `renderedData` field is a +reference to the Metal3Data object created for this machine. If the +dataTemplate field is set but either the `renderedData`, `metaData` or +`networkData` fields in the status are unset, then the Metal3Machine +controller will wait until it can find the Metal3Data object and the rendered +secrets. It will then populate those fields. + +When CAPM3 controller will set the different fields in the BareMetalHost, +it will reference the metadata secret and the network data secret +in the BareMetalHost. If any of the `metaData` or `networkData` status fields +are unset, that field will also remain unset on the BareMetalHost. + +When the Metal3Machine gets deleted, the CAPM3 controller will remove its +ownerreference from the data template object. This will trigger the deletion of +the generated Metal3Data object and the secrets generated for this machine. + +### hostSelector Examples + +The `hostSelector field has two possible optional sub-fields: + +* **matchLabels** -- Key/value pairs of labels that must match exactly. + +* **matchExpressions** -- A set of expressions that must evaluate to true for + the labels on a `BareMetalHost`. + +Valid operators include: + +* **!** -- Key does not exist. Values ignored. +* **=** -- Key equals specified value. There must only be one + value specified. +* **==** -- Key equals specified value. There must only be one + value specified. +* **in** -- Value is a member of a set of possible values +* **!=** -- Key does not equal the specified value. There must + only be one value specified. +* **notin** -- Value not a member of the specified set of values. +* **exists** -- Key exists. Values ignored. +* **gt** -- Value is greater than the one specified. Value must be + an integer. +* **lt** -- Value is less than the one specified. Value must be + an integer. + +Example 1: Only consider a `BareMetalHost` with label `key1` set to `value1`. + +```yaml +spec: + providerSpec: + value: + hostSelector: + matchLabels: + key1: value1 +``` + +Example 2: Only consider `BareMetalHost` with both `key1` set to `value1` AND +`key2` set to `value2`. + +```yaml +spec: + providerSpec: + value: + hostSelector: + matchLabels: + key1: value1 + key2: value2 +``` + +Example 3: Only consider `BareMetalHost` with `key3` set to either `a`, `b`, or +`c`. + +```yaml +spec: + providerSpec: + value: + hostSelector: + matchExpressions: + - key: key3 + operator: in + values: [‘a’, ‘b’, ‘c’] +``` + +Example 3: Only consider `BareMetalHost` with `key1` set to `value1` AND `key2` +set to `value2` AND `key3` set to either `a`, `b`, or `c`. + +```yaml +spec: + providerSpec: + value: + hostSelector: + matchLabels: + key1: value1 + key2: value2 + matchExpressions: + - key: key3 + operator: in + values: [‘a’, ‘b’, ‘c’] +``` + +### Metal3Machine example + +```yaml +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 +kind: Metal3Machine +metadata: + name: controlplane-0 +spec: + image: + url: https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img + checksum: https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img.md5sum + hostSelector: + matchLabels: + key1: value1 + matchExpressions: + key: key2 + operator: in + values: {‘abc’, ‘123’, ‘value2’} + dataTemplate: + Name: controlplane-metadata + metaData: + Name: controlplane-0-metadata-0 +``` + +## MachineDeployment + +MachineDeployment is a core Cluster API object that is similar to +deployment for pods. It refers to a KubeadmConfigTemplate and to a +Metal3MachineTemplate. + +Example MachineDeployment: + +```yaml +apiVersion: cluster.x-k8s.io/v1alpha3 +kind: MachineDeployment +metadata: + name: md-0 + labels: + cluster.x-k8s.io/cluster-name: cluster + nodepool: nodepool-0 +spec: + replicas: 1 + selector: + matchLabels: + cluster.x-k8s.io/cluster-name: cluster + nodepool: nodepool-0 + template: + metadata: + labels: + cluster.x-k8s.io/cluster-name: cluster + nodepool: nodepool-0 + spec: + version: 1.16 + bootstrap: + configRef: + name: md-0 + apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3 + kind: KubeadmConfigTemplate + infrastructureRef: + name: md-0 + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 + kind: Metal3MachineTemplate +``` + +## KubeadmConfigTemplate + +This contains a template to generate KubeadmConfig. + +Example KubeadmConfigTemplate: + +```yaml +apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3 +kind: KubeadmConfigTemplate +metadata: + name: md-0 +spec: + template: + spec: + joinConfiguration: + nodeRegistration: + name: '{{ ds.meta_data.name }}' + kubeletExtraArgs: + node-labels: 'metal3.io/uuid={{ ds.meta_data.uuid }}' + preKubeadmCommands: + - apt update -y + - >- + apt install apt-transport-https ca-certificates curl gnupg-agent + software-properties-common -y + - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - + - >- + add-apt-repository "deb [arch=amd64] + https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" + - apt update -y + - apt install docker-ce docker-ce-cli containerd.io -y + - usermod -aG docker ubuntu + - >- + curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg + | apt-key add - + - >- + echo 'deb https://apt.kubernetes.io/ kubernetes-xenial main' + > /etc/apt/sources.list.d/kubernetes.list + - apt update + - apt install -y kubelet kubeadm kubectl + - systemctl enable --now kubelet +``` + +## Metal3MachineTemplate + +The Metal3MachineTemplate contains the template to create Metal3Machine. + +Example Metal3MachineTemplate : + +```yaml +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 +kind: Metal3MachineTemplate +metadata: + name: md-0 +spec: + template: + spec: + image: + url: https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img + checksum: https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img.md5sum + hostSelector: + matchLabels: + key1: value1 + matchExpressions: + key: key2 + operator: in + values: {‘abc’, ‘123’, ‘value2’} + dataTemplate: + Name: md-0-metadata +``` + +## Metal3DataTemplate + +```yaml +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 +kind: Metal3DataTemplate +metadata: + name: nodepool-1 + namespace: default + ownerReferences: + - apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 + controller: true + kind: Metal3Cluster + name: cluster-1 +spec: + metaData: + strings: + - key: abc + value: def + objectNames: + - key: name_m3m + object: metal3machine + - key: name_machine + object: machine + - key: name_bmh + object: baremetalhost + indexes: + - key: index + offset: 0 + step: 1 + ipAddesses: + - key: ip + start: 192.168.0.10 + end: 192.168.0.100 + subnet: 192.168.0.0/24 + step: 1 + fromHostInterfaces: + - key: mac + interface: "eth0" + fromLabels: + - key: label-1 + object: machine + label: mylabelkey + fromAnnotations: + - key: annotation-1 + object: machine + annotation: myannotationkey + networkData: + links: + ethernets: + - type: "phy" + id: "enp1s0" + mtu: 1500 + macAddress: + fromHostInterface: "eth0" + - type: "phy" + id: "enp2s0" + mtu: 1500 + macAddress: + fromHostInterface: "eth1" + bonds: + - id: "bond0" + mtu: 1500 + macAddress: + string: "XX:XX:XX:XX:XX:XX" + bondMode: "802.1ad" + bondLinks: + - enp1s0 + - enp2s0 + vlans: + - id: "vlan1" + mtu: 1500 + macAddress: + string: "YY:YY:YY:YY:YY:YY" + vlanId: 1 + vlanLink: bond0 + networks: + ipv4DHCP: + - id: "provisioning" + link: "bond0" + + ipv4: + - id: "Baremetal" + link: "vlan1" + ipAddress: + start: "192.168.0.10" + end: "192.168.0.100" + subnet: "192.168.0.0/24" + step: 1 + netmask: 24 + routes: + - network: "0.0.0.0" + netmask: 0 + gateway: "192.168.0.1" + services: + - type: "dns" + address: "8.8.4.4" + ipv6DHCP: + - id: "provisioning6" + link: "bond0" + ipv6SLAAC: + - id: "provisioning6slaac" + link: "bond0" + ipv6: + - id: "Baremetal6" + link: "vlan1" + ipAddress: + start: "2001:0db8:85a3::8a2e:0370:a" + end: "2001:0db8:85a3::8a2e:0370:fff0" + subnet: "2001:0db8:85a3::8a2e:0370:0/64" + step: 10 + netmask: 64 + routes: + - network: "0::0" + netmask: 0 + gateway: "2001:0db8:85a3::8a2e:0370:1" + services: + - dns: "2001:4860:4860::8844" + services: + dns: + - "8.8.8.8" + - "2001:4860:4860::8888" +status: + indexes: + "0": "machine-1" + dataNames: + "machine-1": nodepool-1-0 + lastUpdated: "2020-04-02T06:36:09Z" +``` + +This object will be reconciled by its own controller. When reconciled, +the controller will add a label pointing to the Metal3Cluster that has nodes +linking to this object. The spec contains a `metaData` and a `networkData` field +that contain a template of the values that will be rendered for all nodes. + +The `metaData` field will be rendered into a map of strings in yaml format, +while `networkData` will be rendered into a map equivalent of +[Nova network_data.json](https://docs.openstack.org/nova/latest/user/metadata.html#openstack-format-metadata). +On the target node, the network data will be rendered as a json object that +follows the format definition that can be found +[here](https://docs.openstack.org/nova/latest/_downloads/9119ca7ac90aa2990e762c08baea3a36/network_data.json). + +### Metadata Specifications + +The `metaData` field contains a list of items that will render data in different +ways. The following types of objects are available and accept lists: + +* **strings**: renders the given string as value in the metadata. It takes a + `value` attribute. +* **objectNames** : renders the name of the object that matches the type given. + It takes an `object` attribute, containing the type of the object. +* **indexes**: renders the index of the current object, with the offset from the + `offset` field and using the step from the `step` field. The following + conditions must be matched : `offset` >= 0 and `step` >= 1 + if the step is unspecified (default value being 0), the controller will + automatically change it for 1. The `prefix` and `suffix` attributes are to + provide a prefix and a suffix for the rendered index. +* **ipAddresses**: renders an ip address based on the index, based on the `start` value + if given or using `subnet` to calculate the start value, and checking that + the rendered value is not over the `end` value. The increment is the `step` + value. If the computed value goes out of bounds, the error status will be set + with the error in the error message. In case of using the `subnet` value to + get the start IP address, it will be the second IP of the subnet (for example + `192.168.0.1` for a subnet `192.168.0.0/24`). +* **fromHostInterfaces**: renders the MAC address of the BareMetalHost that + matches the name given as value. +* **fromLabels**: renders the content of a label on an object or an empty string + if the label is absent. It takes an `object` attribute to specify the type of + the object where to fetch the label, and a `label` attribute that contains the + label key. +* **fromAnnotations**: renders the content of a annotation on an object or an + empty string if the annotation is absent. It takes an `object` attribute to + specify the type of the object where to fetch the annotation, and an + `annotation` attribute that contains the annotation key. + +For each object, the attribute **key** is required. + +### networkData specifications + +The `networkData` field will contain three items : + +* **links**: a list of layer 2 interface +* **networks**: a list of layer 3 networks +* **services** : a list of services (DNS) + +#### Links specifications + +The object for the **links** section list can be: + +* **ethernets**: a list of ethernet interfaces +* **bonds**: a list of bond interfaces +* **vlans**: a list of vlan interfaces + +The **links/ethernets** objects contain the following: + +* **type**: Type of the ethernet interface +* **id**: Interface name +* **mtu**: Interface MTU +* **macAddress**: an object to render the MAC Address + +The **links/ethernets/type** can be one of : + +* bridge +* dvs +* hw_veb +* hyperv +* ovs +* tap +* vhostuser +* vif +* phy + +The **links/ethernets/macAddress** object can be one of: + +* **string**: with the desired Mac given as a string +* **fromHostInterface**: with the interface name from BareMetalHost + hardware details. + +The **links/bonds** object contains the following: + +* **id**: Interface name +* **mtu**: Interface MTU +* **macAddress**: an object to render the MAC Address +* **bondMode**: The bond mode +* **bondLinks** : a list of links to use for the bond + +The **links/bonds/bondMode** can be one of : + +* 802.1ad +* balance-rr +* active-backup +* balance-xor +* broadcast +* balance-tlb +* balance-alb + +The **links/vlans** object contains the following: + +* **id**: Interface name +* **mtu**: Interface MTU +* **macAddress**: an object to render the MAC Address +* **vlanId**: The vlan ID +* **vlanLink** : The link on which to create the vlan + +#### The networks specifications + +The object for the **networks** section can be: + +* **ipv4**: a list of ipv4 static allocations +* **ipv4DHCP**: a list of ipv4 DHCP based allocations +* **ipv6**: a list of ipv6 static allocations +* **ipv6DHCP**: a list of ipv6 DHCP based allocations +* **ipv6SLAAC**: a list of ipv6 SLAAC based allocations + +The **networks/ipv4** object contains the following: + +* **id**: the network name +* **link**: The name of the link to configure this network for +* **ipAddress**: the IP address object +* **netmask**: the netmask, in an integer format +* **routes**: the list of route objects + +The **networks/ipv4/ipAddress** is an address object containing: + +* **start**: the start IP address +* **end**: The end IP address +* **subnet**: The subnet in a CIDR notation "X.X.X.X/X" +* **step**: the step between IP addresses + +If the **subnet** is specified, then **start** and **end** are not required and +reverse, if **start** and **end** are specified, then **subnet** is not required + +The **networks/ipv4/routes** is a route object containing: + +* **network**: the subnet to reach +* **netmask**: the mask of the subnet as integer +* **gateway**: the gateway to use +* **services**: a list of services object as defined later + +The **networks/ipv4Dhcp** object contains the following: + +* **id**: the network name +* **link**: The name of the link to configure this network for +* **routes**: the list of route objects + +The **networks/ipv6** object contains the following: + +* **id**: the network name +* **link**: The name of the link to configure this network for +* **ipAddress**: the IP address object +* **netmask**: the netmask, in an integer format +* **routes**: the list of route objects + +The **networks/ipv6Dhcp** object contains the following: + +* **id**: the network name +* **link**: The name of the link to configure this network for +* **routes**: the list of route objects + +The **networks/ipv6Slaac** object contains the following: + +* **id**: the network name +* **link**: The name of the link to configure this network for +* **routes**: the list of route objects + +#### the services specifications + +The object for the **services** section can be: + +* **dns**: a list of dns service with the ip address of a dns server + +## The Metal3Data object + +The output of the controller would be a Metal3Data object,one per node linking to the +Metal3DataTemplate object and the associated secrets + +The Metal3Data object would be: + +```yaml +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 +kind: Metal3Data +metadata: + name: nodepool-1-0 + namespace: default + ownerReferences: + - apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 + controller: true + kind: Metal3DataTemplate + name: nodepool-1 +spec: + index: 0 + metaData: + name: machine-1-metadata + namespace: default + networkData: + name: machine-1-metadata + namespace: default + metal3Machine: + name: machine-1 + namespace: default +status: + ready: true + error: false + errorMessage: "" +``` + +The Metal3Data will contain the index of this node, and links to the secrets +generated and to the Metal3Machine using this Metal3Data object. + +If the Metal3DataTemplate object is updated, the generated secrets will not be +updated, to allow for re-provisioning of the nodes in the exact same state as +they were initially provisioned. Hence, to do an update, it is necessary to do +a rolling upgrade of all nodes. + +The reconciliation of the Metal3DataTemplate object will also be triggered by +changes on Metal3Machines. In the case that a Metal3Machine gets modified, if +the `dataTemplate` references a Metal3DataTemplate, that object will be reconciled. +There will be two cases: + +* An already generated Metal3Data object exists with an ownerReference to this + Metal3Machine. In that case, the reconciler will verify that the required + secrets exist. If they do not, they will be created. +* if no Metal3Data exists with an ownerReference to this Metal3Machine, then the + reconciler will create one and fill the respective field with the secret name. + +To create a Metal3Data object, the Metal3DataTemplate controller will select an +index for that Metal3Machine. The selection happens by selecting the lowest +available index that is not in the `indexes` field of the status. If the +`indexes` field is empty, the controller will list all existing Metal3Data +object linked to this Metal3DataTemplate and recreate the unavailable indexes. +It will fill it by extracting the index from the Metal3Data names. The indexes +always start from 0 and increment by 1. The lowest available index is to be used +next. The `dataNames` field contains the map of Metal3Machine to Metal3Data. + +Once the next lowest available index is found, it will create the Metal3Data +object. The name would be a concatenation of the Metal3DataTemplate name and +index. Upon conflict, it will fetch again the list to consider the new list of +Metal3Data and try to create the new object with the new index, this will happen +until the new object is created successfully. Upon success, it will render the +content values, and create the secrets containing the rendered data. The +controller will generate the content based on the `metaData` or `networkData` +field of the Metal3DataTemplate Specs. + +Once the generation is successful, the status field `ready` will be set to True. +If any error happens during the rendering, an error message will be added. + +## The generated secrets + +The name of the secret will be made of a prefix and the index. The Metal3Machine +object name will be used as the prefix. A `-metadata-` or `-networkdata-` will +be added between the prefix and the index. + +## Metal3 dev env examples + +You can find CR examples in the +[Metal3-io dev env project](https://github.com/metal3-io/metal3-dev-env), +in the [template +folder](https://github.com/metal3-io/metal3-dev-env/tree/master/vm-setup/roles/v1aX_integration_test/templates). diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 00000000..74a69bb2 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,459 @@ +# Architecture + +## Introduction + +The ```cluster-api-provider-metal3 (CAPM3)``` is one of the controllers +involved in managing the life cycle of kubernetes clusters on Metal3 +Machines. This document describes the components involved and their roles. It +also discusses the flow of information from one CR to another with the help of +the controllers. As to avoid ambiguity, we will refer to the physical or virtual +machines managed by the controllers as Bare Metal Servers. And, the kubernetes +resources (CRs) representing them as Metal3 Machines (M3M). + +## Components + +The ```cluster-api-provider-metal3 (CAPM3)``` operator is responsible for +watching and reconciling multiple resources. However, it is important to see +other controllers and custom resources (CRs) involved in the process. The +ultimate goal of the interaction between the controllers and CRs is to provision +a kubernetes cluster on Bare Metal Servers. To that end, the controllers +perform different actions on one or more relevant CRs. + +The following diagram shows the different controllers and CRs involved. The +CAPI, CABPK and CAPM3 controllers are beyond the scope of this document. +With respect to CAPM3, we focus on what CRS it `watches` and `reconciles`. The +arrows in black show which CRs the controller `reconciles` while the one in red +show that a related controller is `watching` another CR. + +![components](images/components.png) + +As shown in the above Components' diagram, CAPM3 is watching for changes in +`Machine` CR and upon change, it makes changes on the `Metal3Machine` CR. +Similarly, it watches `Cluster` CR and makes changes on a related +`Metal3Cluster` CR. + +The left most components, BMO controller and BareMetalHost(BMH) CR, are the +closest to the Bare Metal Server. If one wants to changes the state of a Metal3 +Machine, they modify the BMH CR. Upon change to the BMH, BMO interacts +with Ironic to make changes on the Bare Metal Server. + +During the initial introspection and state changes, the above logic works in the + opposite direction as well. Information gathered during introspection or any + state changes on the Bare Metal Server, results in BMO learning about the + change(s) via ironic and a chain of events starts. Once BMO learns about the + changes, it makes the required +changes on the BMH. + +As discussed above, the management of Bare Metal Servers requires the +interaction of multiple controllers via multiple CRs. However, the interaction +is performed on a specified number of fields on each CR. i.e. A controller +`watches` a specified number of fields in each CR and makes changes on a set of + fields. Before seeing the relationship, we need to see the CRs at two + stages, before and after provisioning a control plane machine. + +--- + +### Cluster + +Cluster, User provided Configuration + +```yaml +apiVersion: cluster.x-k8s.io/v1alpha3 +kind: Cluster +metadata: + name: test1 +spec: + clusterNetwork: + services: + cidrBlocks: ["10.96.0.0/12"] + pods: + cidrBlocks: ["192.168.0.0/18"] + serviceDomain: "cluster.local" + infrastructureRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 + kind: Metal3Cluster + name: test1 +``` + +Cluster, after reconciliation + +```yaml +apiVersion: cluster.x-k8s.io/v1alpha3 +kind: Cluster +metadata: + name: test1 + namespace: metal3 +spec: + clusterNetwork: + pods: + cidrBlocks: + - 192.168.0.0/18 + serviceDomain: cluster.local + services: + cidrBlocks: + - 10.96.0.0/12 + |----------------------------------------------------------------------------| + |# infrastructureRef comes from 'Metal3Cluster' and is added by 'CAPM3' | + | infrastructureRef: | + | apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 | + | kind: Metal3Cluster | + | name: test1 | + |----------------------------------------------------------------------------- +status: + apiEndpoints: + - host: 192.168.111.249 + port: 6443 + controlPlaneInitialized: true + infrastructureReady: true + phase: provisioned +``` + +--- + +### Metal3Cluster + +Metal3Cluster, User provided Configuration for + +```yaml +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 +kind: Metal3Cluster +metadata: + name: test1 +spec: + apiEndpoint: https://192.168.111.249:6443 + noCloudProvider: true +``` + +Metal3Cluster, after reconciliation + +```yaml +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 +kind: Metal3Cluster +metadata: + name: test1 + namespace: metal3 + |----------------------------------------------------------------------------| + |# ownerReferences refers to the linked Cluster and is added by 'CAPM3' | + |ownerReferences: | + |- apiVersion: cluster.x-k8s.io/v1alpha3 | + | kind: Cluster | + | name: test1 | + | uid: 193ec580-89db-46cd-b6f7-ddc0cd79636d | + |----------------------------------------------------------------------------| +spec: + apiEndpoint: https://192.168.111.249:6443 + noCloudProvider: true +status: + apiEndpoints: + - host: 192.168.111.249 + port: 6443 + ready: true +``` + +--- + +### Machine + +Machine, User provided Configuration + +```yaml +apiVersion: cluster.x-k8s.io/v1alpha3 +kind: Machine +metadata: + name: test1-controlplane-0 + labels: + cluster.x-k8s.io/control-plane: "true" + cluster.x-k8s.io/cluster-name: "test1" +spec: + version: v1.17.0 + bootstrap: + configRef: + apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3 + kind: KubeadmConfig + name: test1-controlplane-0 + infrastructureRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 + kind: Metal3Machine + name: test1-controlplane-0 +``` + +Machine, after reconciliation + +```yaml +apiVersion: cluster.x-k8s.io/v1alpha3 +kind: Machine +metadata: + labels: + cluster.x-k8s.io/cluster-name: test1 + cluster.x-k8s.io/control-plane: "true" + name: test1-controlplane-0 + namespace: metal3 + ownerReferences: + - apiVersion: cluster.x-k8s.io/v1alpha3 + kind: Cluster + name: test1 + uid: 193ec580-89db-46cd-b6f7-ddc0cd79636d +spec: + bootstrap: + configRef: + apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3 + kind: KubeadmConfig + name: test1-controlplane-0 + |----------------------------------------------------------------------------| + |# data comes from 'KubeadmConfig.status.bootstrapData' & is added by 'CAPI' | + | data: | + |----------------------------------------------------------------------------| + infrastructureRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 + kind: Metal3Machine + name: test1-controlplane-0 + providerID: metal3://8e16d3b6-d48c-41e0-af0f-e43dbf5ec0cd + version: v1.17.0 +status: + addresses: + - address: 172.22.0.10 + type: InternalIP + - address: 192.168.111.21 + type: InternalIP + - address: node-1 + type: Hostname + - address: node-1 + type: InternalDNS + bootstrapReady: true + infrastructureReady: true + nodeRef: + name: node-1 + phase: running +``` + +--- +### Metal3Machine + +Metal3Machine, User provided Configuration + +```yaml +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 +kind: Metal3Machine +metadata: + name: test1-controlplane-0 +spec: + image: + url: http://172.22.0.1/images/bionic-server-cloudimg-amd64.img + checksum: http://172.22.0.1/images/bionic-server-cloudimg-amd64.img.md5sum +``` + +Metal3Machine, after reconciliation + +```yaml +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 +kind: Metal3Machine +metadata: + name: test1-controlplane-0 + namespace: metal3 + # ownerReferences refers to the linked Machine and is added by 'CAPM3' + ownerReferences: + - apiVersion: cluster.x-k8s.io/v1alpha3 + kind: Machine + name: test1-controlplane-0 +spec: + hostSelector: {} + image: + checksum: http://172.22.0.1/images/bionic-server-cloudimg-amd64.img.md5sum + url: http://172.22.0.1/images/bionic-server-cloudimg-amd64.img + providerID: metal3://8e16d3b6-d48c-41e0-af0f-e43dbf5ec0cd + |----------------------------------------------------------------------------| + |# userData comes from 'Machine' and is added by 'CAPM3' | + |userData: | + | name: test1-controlplane-0-user-data | + | namespace: metal3 | + |----------------------------------------------------------------------------| +status: + addresses: + - address: 172.22.0.10 + type: InternalIP + - address: 192.168.111.21 + type: InternalIP + - address: node-1 + type: Hostname + - address: node-1 + type: InternalDNS + ready: true +``` + +--- + +### BareMetalHost + +BareMetalHost, User provided Configuration + +```yaml +apiVersion: metal3.io/v1alpha1 +kind: BareMetalHost +metadata: + name: node-1 +spec: + online: true + bootMACAddress: 00:b2:8c:ee:22:98 + bmc: + address: ipmi://192.168.111.1:6231 + credentialsName: node-1-bmc-secret +``` + +BareMetalHost, after reconciliation + +```yaml +apiVersion: metal3.io/v1alpha1 +kind: BareMetalHost +metadata: + name: node-1 + namespace: metal3 +spec: + bmc: + address: ipmi://192.168.111.1:6231 + credentialsName: node-1-bmc-secret + bootMACAddress: 00:b2:8c:ee:22:98 + |----------------------------------------------------------------------------| + |# consumerRef refers to the linked Metal3Machine is added by 'CAPM3' | + |consumerRef: | + | apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 | + | kind: Metal3Machine | + | name: test1-controlplane-0 | + | namespace: metal3 | + |# Image comes from 'Metal3Machine' and is added by 'CAPM3' | + |image: | + | checksum: http://172.22.0.1/images/bionic-server-cloudimg-amd64.img.md5sum| + | url: http://172.22.0.1/images/bionic-server-cloudimg-amd64.img | + |online: true | + |# UserData comes from 'Metal3Machine' and is added by 'CAPBK' | + |userData: | + | name: test1-controlplane-0-user-data | + | namespace: metal3 | + |----------------------------------------------------------------------------| +status: + goodCredentials: + credentials: + name: node-1-bmc-secret + namespace: metal3 + hardware: + hardwareProfile: unknown + + operationHistory: .... + operationalStatus: OK + poweredOn: true + provisioning: + ID: 75450b93-0476-45b7-8629-d1eebbdc558b + image: + checksum: http://172.22.0.1/images/bionic-server-cloudimg-amd64.img.md5sum + url: http://172.22.0.1/images/bionic-server-cloudimg-amd64.img + state: provisioned + triedCredentials: + credentials: + name: node-1-bmc-secret + namespace: metal3 + credentialsVersion: "1435" +``` + +### KubeadmConfig + +KubeadmConfig, user provided Configuration + +```yaml +apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3 +kind: KubeadmConfig +metadata: + name: test1-controlplane-0 +spec: + initConfiguration: + nodeRegistration: + name: '{{ ds.meta_data.name }}' + kubeletExtraArgs: + node-labels: 'metal3.io/uuid={{ ds.meta_data.uuid }}' + preKubeadmCommands: + postKubeadmCommands: + files: +``` + +KubeadmConfig, after reconciliation + +```yaml +apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3 +kind: KubeadmConfig +metadata: + name: test1-controlplane-0 + namespace: metal3 + ownerReferences: + - apiVersion: cluster.x-k8s.io/v1alpha3 + kind: Machine + name: test1-controlplane-0 +spec: + clusterConfiguration: + apiServer: {} + apiVersion: kubeadm.k8s.io/v1beta1 + certificatesDir: /etc/kubernetes/pki + clusterName: test1 + controlPlaneEndpoint: 192.168.111.249:6443 + controllerManager: {} + dns: + type: "" + etcd: {} + imageRepository: "" + kind: ClusterConfiguration + kubernetesVersion: v1.17.0 + networking: + dnsDomain: cluster.local + podSubnet: 192.168.0.0/18 + serviceSubnet: 10.96.0.0/12 + scheduler: {} + files: + initConfiguration: + nodeRegistration: + kubeletExtraArgs: + node-labels: metal3.io/uuid={{ ds.meta_data.uuid }} + name: '{{ ds.meta_data.name }}' + postKubeadmCommands: +status: + |----------------------------------------------------------------------------| + |bootstrapData: | + |----------------------------------------------------------------------------| + ready: true +``` + +--- +#### Flow of information + +As was shown on the above CRs, some of the fields are introduced in one CR and +they travel through multiple CRs to reach the BMH, which is the closest to the +Bare Metal Server. There is also a movement of information from the +virtual/physical machines towards the CRs, but this is beyond the scope of this + document. Some of the fields are added by users, while the others are by the + relevant controllers. + +We have added the source of relevant fields as comments in the above yaml files. +The following sequence diagram shows the flow of information (fields) across +multiple CRs with the help of controllers. + +![field mapping](images/controllerssequencediagram.png) + +#### Some relevant fields + +```apiEndpoint:``` IP:Port of a load balancer (keepalived VIP) + +```image:``` OS image for the Metal3 Machine + +The following fields are used to make a relationship between CRs. + +```bash +infrastructureRef +ownerReferences +consumerRef +``` + +The following fields refer to the cloud-init data, but the names change across +different CRs. + +```bash +bootstrap +bootstrap.data +userData +bootstrapData +``` diff --git a/docs/deployment_workflow.md b/docs/deployment_workflow.md new file mode 100644 index 00000000..c82a341a --- /dev/null +++ b/docs/deployment_workflow.md @@ -0,0 +1,86 @@ +# Deployment workflow + +## Deploying the controllers + +The following controllers need to be deployed : + +* CAPI +* CAPBK or alternative +* CACPK or alternative +* CAPM3 +* Baremetal Operator, with Ironic setup + +## Requirements + +The cluster should either : + +* be deployed with an external cloud provider, that would be deployed as part of + the userData and would set the providerIDs based on the BareMetalHost UID. +* Be deployed with the ProviderID set to be the BareMetalHostUUID +* be deployed with each node with the label "metal3.io/uuid" set to the + BareMetalHost UID that is provided by ironic to cloud-init through the + metadata `ds.meta_data.uuid`. This can be achieved by setting the following in + the KubeadmConfig : + +```yaml +nodeRegistration: + name: '{{ ds.meta_data.name }}' + kubeletExtraArgs: + node-labels: 'metal3.io/uuid={{ ds.meta_data.uuid }}' +``` + +## Deploying the CRs + +You can deploy the CRs all at the same time, the controllers will take care of +following the correct flow. +An outline of the workflow is below. + +1. The CAPI controller will set the OwnerRef on the Metal3Cluster referenced + by the Cluster, on the KubeadmControlPlane, and all machines, KubeadmConfig + and Metal3Machines created by the user or by a MachineDeployment. +1. The CAPM3 controller will verify the controlPlaneEndpoint field and populate + the status with ready field set to true. +1. The CAPI controller will set infrastructureReady field to true on the Cluster +1. The CAPI controller will set the OwnerRef +1. The KubeadmControlPlane controller will wait until the cluster has + infrastructureReady set to true, and generate the first machine, + Metal3Machine and KubeadmConfig. +1. CABPK will generate the cloud-init output for this machine and create a + secret containing it. +1. The CAPI controller will copy the userData secret name into the machine + object and set the bootstrapReady field to true. +1. Once the secrets exists, the CAPM3 controller will select, if + possible, a BareMetalHost that matches the criteria, or wait until one is + available. +1. Once the machine has been associated with a BaremetalHost, the CAPM3 + controller will check if the Metal3Machine references a + Metal3DataTemplate object. In that case, it will set an OwnerReference on the + Metal3DataTemplate object referencing the Metal3Machine and wait for the + metadata and/or network data secrets to be created. +1. The CAPM3 controller reconciling the Metal3DataTemplate object will select + the lowest available index for the new machine and create a Metal3Data + object that will then create the secrets containing the rendered data. +1. The CAPM3 controller will then set the BareMetalHost spec accordingly to the + Metal3Machine specs. +1. The BareMetal Operator will then start the deployment. +1. After deployment, the BaremetalHost will be in provisioned state. However, + initialization is not complete. If deploying without cloud provider, CAPM3 + can wait until the target cluster is up and the node appears, then fetch + the node by matching the label `metal3.io/uuid=` and set the + providerID to `metal3://`. The Metal3Machine ready status will + be set to true and the providerID will be set to `metal3://` on the + Metal3Machine. +1. CAPI will access the target cluster and compare the providerID on the node to + the providerID of the Machine, copied from the metal3machine. If matching, + the control plane initialized status will be set to true and the machine + state to running. +1. CACPK will then do the same for each further controller node until reaching + the desired replicas number, one by one, triggering the same workflow. + Meanwhile, as soon as the controlplane is initialized, CABPK will generate + the user data for all other machines, triggering their deployment. + +## Deletion + +Deleting the cluster object will trigger the deletion of all related objects +except for KubeadmConfigTemplates, Metal3MachineTemplates, Metal3DataTemplates +and BareMetalHosts, and the secrets related to the BareMetalHosts. diff --git a/docs/dev-setup.md b/docs/dev-setup.md new file mode 100644 index 00000000..488b4ac9 --- /dev/null +++ b/docs/dev-setup.md @@ -0,0 +1,75 @@ +# Setting up a development environment + +## Pre-requisites + +CAPM3 requires two external tools for running the tests +during development. + +### Install kustomize + +```bash +./hack/tools/install_kustomize.sh +``` + +### Install kubebuilder + +```bash +./hack/tools/install_kubebuilder.sh +``` + +## Development using Kind or Minikube + +See the [Kind docs](https://kind.sigs.k8s.io/docs/user/quick-start) for +instructions on launching a Kind cluster and the +[Minikube docs](https://kubernetes.io/docs/setup/minikube/) for +instructions on launching a Minikube cluster. + +### Add CRDs and CRs from baremetal-operator + +The provider also uses the `BareMetalHost` custom resource that’s defined by +the `baremetal-operator`. The following command deploys the CRD and creates +dummy BareMetalHosts. + +```sh + make deploy-bmo-cr +``` + +When a `Metal3Machine` gets created, the provider looks for an available +`BareMetalHost` to claim and then sets it to be provisioned to fulfill the +request expressed by the `Metal3Machine`. Before creating a +`Metal3Machine`, we can create a dummy `BareMetalHost` object. There’s no +requirement to actually run the +`baremetal-operator` to test the reconciliation logic of the provider. + +Refer to the [baremetal-operator developer +documentation](https://github.com/metal3-io/baremetal-operator/blob/master/docs/dev-setup.md) +for instructions and tools for creating BareMetalHost objects. + +### Deploy CAPI and CAPM3 + +The following command will deploy the controllers from CAPI, CABPK and CAPM3 and +the requested CRDs. + +```sh + make deploy +``` + +### Run the Controller locally + +You will first need to scale down the controller deployment : + +```sh + kubectl scale -n capm3-system deployment.v1.apps/capm3-controller-manager \ + --replicas 0 +``` + +You can manually run the controller from outside of the cluster for development +and testing purposes. There’s a `Makefile` target which makes this easy. + +```bash +make run +``` + +You can follow the output on the console to see information about what the +controller is doing. You can also proceed to create/update/delete +`Metal3Machines` and `BareMetalHosts` to test the controller logic. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 00000000..92d176b6 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,219 @@ +# Cluster API provider Bare Metal + +This provider integrates with the +[Cluster API project](https://github.com/kubernetes-sigs/cluster-api). + +## Setup + +### Pre-requisites + +The pre-requisite for the deployment of CAPM3 are the following: + +- [Baremetal-Operator](https://github.com/metal3-io/baremetal-operator) deployed +- Ironic up and running (inside or outside of the cluster) +- BareMetalHost resources created for all hardware nodes and in "ready" or + "available" state +- If deploying CAPI in a multi-tenancy scenario, all cluster-related CRs (inc. + BareMetalHosts and related) must be in the same namespace. This is due to the + fact that the controllers are restricted to their own namespace with RBAC. + +### Using clusterctl + +Please refer to +[Clusterctl documentation](https://master.cluster-api.sigs.k8s.io/clusterctl/overview.html). +Once the Pre-requisites are fulfilled, you can follow the normal clusterctl +flow for the `init`, `config`, `upgrade` and `delete` workflow. The `move` +command is supported only if the baremetalhosts are moved independently first, +and their status is preserved during the move. Please refer to the *Pivoting +Ironic* section. + +### Cluster templates variables + +You can find an example file containing the environment variables +`example_variables.rc`in the release or +[here](https://github.com/metal3-io/cluster-api-provider-metal3/tree/master/examples/clusterctl-templates/example_variables.rc) + +#### POD_CIDR + +This is the CIDR for the pod. It can be given as a comma separated list of +quoted elements. For example: + +`POD_CIDR='"192.168.0.0/24", "192.168.1.0/24"'` + +#### SERVICE_CIDR + +This is the CIDR for the services. It can be given as a comma separated list of +quoted elements. For example: + +`SERVICE_CIDR='"192.168.2.0/24", "192.168.3.0/24"'` + +#### API_ENDPOINT_HOST + +This is the API endpoint name or IP address. For example: + +`API_ENDPOINT_HOST="192.168.111.249"` + +#### API_ENDPOINT_PORT + +This is the API endpoint port. For example: + +`API_ENDPOINT_PORT="6443"` + +#### IMAGE_URL + +This is the URL of the image to deploy. It should be a qcow2 image. For example: + +`IMAGE_URL="http://192.168.0.1/ubuntu.qcow2"` + +#### IMAGE_CHECKSUM + +This is the URL of the md5sum of the image to deploy. For example: + +`IMAGE_CHECKSUM="http://192.168.0.1/ubuntu.qcow2.md5sum"` + +#### CTLPLANE_KUBEADM_EXTRA_CONFIG + +This contains the extra configuration to pass in KubeadmControlPlane. It is +critical to maintain the indentation. The allowed keys are : + +- preKubeadmCommands +- postKubeadmCommands +- files +- users +- ntp +- format + +Here is an example for Ubuntu: + +```bash +CTLPLANE_KUBEADM_EXTRA_CONFIG=" + preKubeadmCommands: + - ip link set dev enp2s0 up + - dhclient enp2s0 + - apt update -y + - netplan apply + - >- + apt install net-tools gcc linux-headers-$(uname -r) bridge-utils + apt-transport-https ca-certificates curl gnupg-agent + software-properties-common -y + - apt install -y keepalived && systemctl stop keepalived + - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - + - add-apt-repository \"deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\" + - curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - + - echo 'deb https://apt.kubernetes.io/ kubernetes-xenial main' > /etc/apt/sources.list.d/kubernetes.list + - apt update -y + - apt install docker-ce docker-ce-cli containerd.io kubelet kubeadm kubectl -y + - systemctl enable --now docker kubelet + - if (curl -sk --max-time 10 https://{{ CLUSTER_APIENDPOINT_HOST }}:6443/healthz); then echo \"keepalived already running\";else systemctl start keepalived; fi + - usermod -aG docker ubuntu + postKubeadmCommands: + - mkdir -p /home/ubuntu/.kube + - cp /etc/kubernetes/admin.conf /home/ubuntu/.kube/config + - systemctl enable --now keepalived + - chown ubuntu:ubuntu /home/ubuntu/.kube/config + files: + - path: /etc/keepalived/keepalived.conf + content: | + ! Configuration File for keepalived + global_defs { + notification_email { + sysadmin@example.com + support@example.com + } + notification_email_from lb@example.com + smtp_server localhost + smtp_connect_timeout 30 + } + vrrp_instance VI_2 { + state MASTER + interface enp2s0 + virtual_router_id 2 + priority 101 + advert_int 1 + virtual_ipaddress { + {{ CLUSTER_APIENDPOINT_HOST }} + } + } + - path: /etc/netplan/50-cloud-init.yaml + owner: root:root + permissions: '0644' + content: | + network: + ethernets: + enp2s0: + dhcp4: true + version: 2 + - path : /etc/netplan/60-ironicendpoint.yaml + owner: root:root + permissions: '0644' + content: | + network: + version: 2 + renderer: networkd + bridges: + ironicendpoint: + interfaces: [enp1s0] + dhcp4: yes +" +``` + +#### WORKERS_KUBEADM_EXTRA_CONFIG + +This contains the extra configuration to pass in KubeadmConfig for workers. It +is critical to maintain the indentation. The allowed keys are : + +- preKubeadmCommands +- postKubeadmCommands +- files +- users +- ntp +- format + +Here is an example for Ubuntu: + +```bash +WORKERS_KUBEADM_EXTRA_CONFIG=" + preKubeadmCommands: + - ip link set dev enp2s0 up + - dhclient enp2s0 + - apt update -y + - netplan apply + - >- + apt install apt-transport-https ca-certificates + curl gnupg-agent software-properties-common -y + - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - + - add-apt-repository \"deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\" + - curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - + - echo 'deb https://apt.kubernetes.io/ kubernetes-xenial main' > /etc/apt/sources.list.d/kubernetes.list + - apt update -y + - apt install docker-ce docker-ce-cli containerd.io kubelet kubeadm kubectl -y + - systemctl enable --now docker kubelet + - usermod -aG docker ubuntu + files: + - path: /etc/netplan/50-cloud-init.yaml + owner: root:root + permissions: '0644' + content: | + network: + ethernets: + enp1s0: + dhcp4: true + enp2s0: + dhcp4: true + version: 2 +" +``` + +## Pivoting Ironic + +Before running the move command of Clusterctl, elements such as Baremetal +Operator, Ironic if applicable, and the BareMetalHost CRs need to be moved to +the target cluster. During the move, the BareMetalHost object must be annotated +with `baremetalhost.metal3.io/paused` key. The value does not matter. The +presence of this annotation will stop the reconciliation loop for that object. + +In addition, it is critical that the move of the BMHs does not lead to a lost +status for those objects. If the status is lost, BMO will register the nodes +as available, and introspect them again. + +More information TBA on move commands. diff --git a/docs/images/components.png b/docs/images/components.png new file mode 100644 index 0000000000000000000000000000000000000000..592e10b7264a83c63e090c186668e9eaa34101e0 GIT binary patch literal 90895 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/images/controllerssequencediagram.png b/docs/images/controllerssequencediagram.png new file mode 100644 index 0000000000000000000000000000000000000000..9423577eb34895e4ea40317e7e33a6115bedac98 GIT binary patch literal 55662 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 Jb{_ioe*t2{t=Rwo literal 0 HcmV?d00001 diff --git a/docs/images/fields_mapping.png b/docs/images/fields_mapping.png new file mode 100644 index 0000000000000000000000000000000000000000..fdb2a12651a1d2b1a9e4f53bda76ad8f05798082 GIT binary patch literal 48250 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 aJ^u8=U2&yBZr0U;k2e;-6;u_V`tTo>#Y=bq literal 0 HcmV?d00001 diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 00000000..1aad7d13 --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,87 @@ +# Releasing + + + + +- [Prerequisites](#prerequisites) + - [`docker`](#docker) +- [Output](#output) + - [Expected artifacts](#expected-artifacts) + - [Artifact locations](#artifact-locations) +- [Process](#process) + - [Permissions](#permissions) + + + +## Prerequisites + +### `docker` + +You must have docker installed. + +## Output + +### Expected artifacts + +1. A container image of the shared cluster-api-provider-metal3 manager +1. A git tag +1. A release on Github containing: + - A manifest file - `infrastructure-components.yaml` + - A metadata file - `metadata.yaml` + - A cluster template - `cluster-template.yaml` + +### Artifact locations + +1. The container image is found in the registry `quay.io/metal3-io` with an image + name of `cluster-api-provider-metal3` and a tag that matches the release + version. The image is automatically built once the release has been created. + +## Creating a release for CAPM3 + +### Process + +For version v0.x.y: + +1. Create the release notes `make release-notes`. Copy the output and sort + manually the items that need to be sorted. +1. Create an annotated tag `git tag -a v0.x.y -m v0.x.y`. To use your GPG + signature when pushing the tag, use `git tag -s [...]` instead +1. Push the tag to the GitHub repository `git push origin v0.x.y` + NB: `origin` should be the name of the remote pointing to + `github.com/metal3-io/cluster-api-provider-metal3` +1. Run `make release` to build artifacts (the image is automatically built by CI) +1. [Create a release in GitHub](https://help.github.com/en/github/administering-a-repository/creating-releases) + that contains the elements listed above that have been created in the `out` + folder +1. Create a branch `release-0.x` for a minor release for backports and bug fixes. + +### Permissions + +Releasing requires a particular set of permissions. + +- Tag push access to the GitHub repository +- GitHub Release creation access + +## Impact on Metal3 + +Multiple additional actions are required in the Metal3 project + +### Update the Jenkins jobs + +For each minor or major release, two jobs need to be created : + +- a master job that runs on a regular basis +- a PR verification job that is triggered by a keyword on a PR targeted for that + release branch. + +### Update Metal3-dev-env + +Metal3-dev-env variables need to be modified. After a major or minor release, +the new minor version (that follows CAPI versioning) should point to master for +CAPM3 and the released version should point to the release branch. + +### Update the image of CAPM3 in the release branch + +If you just created a release branch (i.e. minor version release), you should +modify the image for CAPM3 deployment in this branch to be tagged with the +branch name. The image will then follow the branch. diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 00000000..90b59cb1 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,62 @@ +# CAPM3 testing + +This document outlines the testing strategy applied in CAPM3. All developers +should follow those guidelines to ensure uniformity and quality in the tests. + +## Code coverage + +The minimum code coverage required for the `api`, `baremetal` and `controllers` +folders is **80%**. Any PR introducing new code should include tests for the new +code and the developers should verify that the code coverage did not decrease +due to their PR. The generated code should be excluded from the coverage (files +starting with `zz_generated.`) + +In order to run the tests and get the cover profile, you can use make. You can +use the auto-completion of the make command to see the available targets. + +```sh +make test +``` + +or + +```sh +make unit +``` + +You can also run the tests in a container exactly as they are run in CI : + +```sh +./hack/unit.sh +``` + +## Testing files + +For each file in each package, a test file should be created, named after the +original file `_test.go`, containing the unit tests for the functions +defined in the file. + +A test file can also be created to contain integration tests, named after the +original file `_integration_test.go`. This file should only contain +integration tests, for example to test the controllers with the managers. + +## Mocking + +To mock the interface, we use mockgen and the gomock module to write the tests. +The mock code should be placed in a folder `mocks` within the folder containing +the mocked code. The mock file should be named after the original file +containing the interface, `zz_generated..go`, and the package +should be named after the original package, `_mocks` + +Unit tests should be performed by mocking the interface of other modules, or +using fake clients from controller-runtime package +`sigs.k8s.io/controller-runtime/pkg/client/fake` or +`"k8s.io/client-go/kubernetes/fake"`. + +## Testing Guidelines + +* The tests should use Ginkgo and Gomega, for example for the assertions, when + needed. Other libraries should not be used to keep uniformity in the tests. +* The tests should be table-driven when there are multiple cases. +* Table-driven tests should use subtests to improve the readability of the + results diff --git a/examples/addons.yaml b/examples/addons.yaml new file mode 100644 index 00000000..4490c96a --- /dev/null +++ b/examples/addons.yaml @@ -0,0 +1,783 @@ +--- +# Source: calico/templates/calico-config.yaml +# This ConfigMap is used to configure a self-hosted Calico installation. +kind: ConfigMap +apiVersion: v1 +metadata: + name: calico-config + namespace: kube-system +data: + # Typha is disabled. + typha_service_name: "none" + # Configure the backend to use. + calico_backend: "bird" + + # Configure the MTU to use + veth_mtu: "1440" + + # The CNI network configuration to install on each node. The special + # values in this config will be automatically populated. + cni_network_config: |- + { + "name": "k8s-pod-network", + "cniVersion": "0.3.1", + "plugins": [ + { + "type": "calico", + "log_level": "info", + "datastore_type": "kubernetes", + "nodename": "__KUBERNETES_NODE_NAME__", + "mtu": __CNI_MTU__, + "ipam": { + "type": "calico-ipam" + }, + "policy": { + "type": "k8s" + }, + "kubernetes": { + "kubeconfig": "__KUBECONFIG_FILEPATH__" + } + }, + { + "type": "portmap", + "snat": true, + "capabilities": {"portMappings": true} + } + ] + } + +--- +# Source: calico/templates/kdd-crds.yaml +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: felixconfigurations.crd.projectcalico.org +spec: + scope: Cluster + group: crd.projectcalico.org + version: v1 + names: + kind: FelixConfiguration + plural: felixconfigurations + singular: felixconfiguration +--- + +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: ipamblocks.crd.projectcalico.org +spec: + scope: Cluster + group: crd.projectcalico.org + version: v1 + names: + kind: IPAMBlock + plural: ipamblocks + singular: ipamblock + +--- + +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: blockaffinities.crd.projectcalico.org +spec: + scope: Cluster + group: crd.projectcalico.org + version: v1 + names: + kind: BlockAffinity + plural: blockaffinities + singular: blockaffinity + +--- + +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: ipamhandles.crd.projectcalico.org +spec: + scope: Cluster + group: crd.projectcalico.org + version: v1 + names: + kind: IPAMHandle + plural: ipamhandles + singular: ipamhandle + +--- + +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: ipamconfigs.crd.projectcalico.org +spec: + scope: Cluster + group: crd.projectcalico.org + version: v1 + names: + kind: IPAMConfig + plural: ipamconfigs + singular: ipamconfig + +--- + +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: bgppeers.crd.projectcalico.org +spec: + scope: Cluster + group: crd.projectcalico.org + version: v1 + names: + kind: BGPPeer + plural: bgppeers + singular: bgppeer + +--- + +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: bgpconfigurations.crd.projectcalico.org +spec: + scope: Cluster + group: crd.projectcalico.org + version: v1 + names: + kind: BGPConfiguration + plural: bgpconfigurations + singular: bgpconfiguration + +--- + +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: ippools.crd.projectcalico.org +spec: + scope: Cluster + group: crd.projectcalico.org + version: v1 + names: + kind: IPPool + plural: ippools + singular: ippool + +--- + +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: hostendpoints.crd.projectcalico.org +spec: + scope: Cluster + group: crd.projectcalico.org + version: v1 + names: + kind: HostEndpoint + plural: hostendpoints + singular: hostendpoint + +--- + +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: clusterinformations.crd.projectcalico.org +spec: + scope: Cluster + group: crd.projectcalico.org + version: v1 + names: + kind: ClusterInformation + plural: clusterinformations + singular: clusterinformation + +--- + +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: globalnetworkpolicies.crd.projectcalico.org +spec: + scope: Cluster + group: crd.projectcalico.org + version: v1 + names: + kind: GlobalNetworkPolicy + plural: globalnetworkpolicies + singular: globalnetworkpolicy + +--- + +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: globalnetworksets.crd.projectcalico.org +spec: + scope: Cluster + group: crd.projectcalico.org + version: v1 + names: + kind: GlobalNetworkSet + plural: globalnetworksets + singular: globalnetworkset + +--- + +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: networkpolicies.crd.projectcalico.org +spec: + scope: Namespaced + group: crd.projectcalico.org + version: v1 + names: + kind: NetworkPolicy + plural: networkpolicies + singular: networkpolicy + +--- + +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: networksets.crd.projectcalico.org +spec: + scope: Namespaced + group: crd.projectcalico.org + version: v1 + names: + kind: NetworkSet + plural: networksets + singular: networkset +--- +# Source: calico/templates/rbac.yaml + +# Include a clusterrole for the kube-controllers component, +# and bind it to the calico-kube-controllers serviceaccount. +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: calico-kube-controllers +rules: + # Nodes are watched to monitor for deletions. + - apiGroups: [""] + resources: + - nodes + verbs: + - watch + - list + - get + # Pods are queried to check for existence. + - apiGroups: [""] + resources: + - pods + verbs: + - get + # IPAM resources are manipulated when nodes are deleted. + - apiGroups: ["crd.projectcalico.org"] + resources: + - ippools + verbs: + - list + - apiGroups: ["crd.projectcalico.org"] + resources: + - blockaffinities + - ipamblocks + - ipamhandles + verbs: + - get + - list + - create + - update + - delete + # Needs access to update clusterinformations. + - apiGroups: ["crd.projectcalico.org"] + resources: + - clusterinformations + verbs: + - get + - create + - update +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: calico-kube-controllers +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: calico-kube-controllers +subjects: +- kind: ServiceAccount + name: calico-kube-controllers + namespace: kube-system +--- +# Include a clusterrole for the calico-node DaemonSet, +# and bind it to the calico-node serviceaccount. +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: calico-node +rules: + # The CNI plugin needs to get pods, nodes, and namespaces. + - apiGroups: [""] + resources: + - pods + - nodes + - namespaces + verbs: + - get + - apiGroups: [""] + resources: + - endpoints + - services + verbs: + # Used to discover service IPs for advertisement. + - watch + - list + # Used to discover Typhas. + - get + - apiGroups: [""] + resources: + - nodes/status + verbs: + # Needed for clearing NodeNetworkUnavailable flag. + - patch + # Calico stores some configuration information in node annotations. + - update + # Watch for changes to Kubernetes NetworkPolicies. + - apiGroups: ["networking.k8s.io"] + resources: + - networkpolicies + verbs: + - watch + - list + # Used by Calico for policy information. + - apiGroups: [""] + resources: + - pods + - namespaces + - serviceaccounts + verbs: + - list + - watch + # The CNI plugin patches pods/status. + - apiGroups: [""] + resources: + - pods/status + verbs: + - patch + # Calico monitors various CRDs for config. + - apiGroups: ["crd.projectcalico.org"] + resources: + - globalfelixconfigs + - felixconfigurations + - bgppeers + - globalbgpconfigs + - bgpconfigurations + - ippools + - ipamblocks + - globalnetworkpolicies + - globalnetworksets + - networkpolicies + - networksets + - clusterinformations + - hostendpoints + verbs: + - get + - list + - watch + # Calico must create and update some CRDs on startup. + - apiGroups: ["crd.projectcalico.org"] + resources: + - ippools + - felixconfigurations + - clusterinformations + verbs: + - create + - update + # Calico stores some configuration information on the node. + - apiGroups: [""] + resources: + - nodes + verbs: + - get + - list + - watch + # These permissions are only requried for upgrade from v2.6, and can + # be removed after upgrade or on fresh installations. + - apiGroups: ["crd.projectcalico.org"] + resources: + - bgpconfigurations + - bgppeers + verbs: + - create + - update + # These permissions are required for Calico CNI to perform IPAM allocations. + - apiGroups: ["crd.projectcalico.org"] + resources: + - blockaffinities + - ipamblocks + - ipamhandles + verbs: + - get + - list + - create + - update + - delete + - apiGroups: ["crd.projectcalico.org"] + resources: + - ipamconfigs + verbs: + - get + # Block affinities must also be watchable by confd for route aggregation. + - apiGroups: ["crd.projectcalico.org"] + resources: + - blockaffinities + verbs: + - watch + # The Calico IPAM migration needs to get daemonsets. These permissions can be + # removed if not upgrading from an installation using host-local IPAM. + - apiGroups: ["apps"] + resources: + - daemonsets + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: calico-node +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: calico-node +subjects: +- kind: ServiceAccount + name: calico-node + namespace: kube-system + +--- +# Source: calico/templates/calico-node.yaml +# This manifest installs the calico-node container, as well +# as the CNI plugins and network config on +# each master and worker node in a Kubernetes cluster. +kind: DaemonSet +apiVersion: apps/v1 +metadata: + name: calico-node + namespace: kube-system + labels: + k8s-app: calico-node +spec: + selector: + matchLabels: + k8s-app: calico-node + updateStrategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 1 + template: + metadata: + labels: + k8s-app: calico-node + annotations: + # This, along with the CriticalAddonsOnly toleration below, + # marks the pod as a critical add-on, ensuring it gets + # priority scheduling and that its resources are reserved + # if it ever gets evicted. + scheduler.alpha.kubernetes.io/critical-pod: '' + spec: + nodeSelector: + beta.kubernetes.io/os: linux + hostNetwork: true + tolerations: + # Make sure calico-node gets scheduled on all nodes. + - effect: NoSchedule + operator: Exists + # Mark the pod as a critical add-on for rescheduling. + - key: CriticalAddonsOnly + operator: Exists + - effect: NoExecute + operator: Exists + serviceAccountName: calico-node + # Minimize downtime during a rolling upgrade or deletion; tell Kubernetes to do a "force + # deletion": https://kubernetes.io/docs/concepts/workloads/pods/pod/#termination-of-pods. + terminationGracePeriodSeconds: 0 + priorityClassName: system-node-critical + initContainers: + # This container performs upgrade from host-local IPAM to calico-ipam. + # It can be deleted if this is a fresh installation, or if you have already + # upgraded to use calico-ipam. + - name: upgrade-ipam + image: calico/cni:v3.8.2 + command: ["/opt/cni/bin/calico-ipam", "-upgrade"] + env: + - name: KUBERNETES_NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + - name: CALICO_NETWORKING_BACKEND + valueFrom: + configMapKeyRef: + name: calico-config + key: calico_backend + volumeMounts: + - mountPath: /var/lib/cni/networks + name: host-local-net-dir + - mountPath: /host/opt/cni/bin + name: cni-bin-dir + # This container installs the CNI binaries + # and CNI network config file on each node. + - name: install-cni + image: calico/cni:v3.8.2 + command: ["/install-cni.sh"] + env: + # Name of the CNI config file to create. + - name: CNI_CONF_NAME + value: "10-calico.conflist" + # The CNI network config to install on each node. + - name: CNI_NETWORK_CONFIG + valueFrom: + configMapKeyRef: + name: calico-config + key: cni_network_config + # Set the hostname based on the k8s node name. + - name: KUBERNETES_NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + # CNI MTU Config variable + - name: CNI_MTU + valueFrom: + configMapKeyRef: + name: calico-config + key: veth_mtu + # Prevents the container from sleeping forever. + - name: SLEEP + value: "false" + volumeMounts: + - mountPath: /host/opt/cni/bin + name: cni-bin-dir + - mountPath: /host/etc/cni/net.d + name: cni-net-dir + # Adds a Flex Volume Driver that creates a per-pod Unix Domain Socket to allow Dikastes + # to communicate with Felix over the Policy Sync API. + - name: flexvol-driver + image: calico/pod2daemon-flexvol:v3.8.2 + volumeMounts: + - name: flexvol-driver-host + mountPath: /host/driver + containers: + # Runs calico-node container on each Kubernetes node. This + # container programs network policy and routes on each + # host. + - name: calico-node + image: calico/node:v3.8.2 + env: + # Use Kubernetes API as the backing datastore. + - name: DATASTORE_TYPE + value: "kubernetes" + # Wait for the datastore. + - name: WAIT_FOR_DATASTORE + value: "true" + # Set based on the k8s node name. + - name: NODENAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + # Choose the backend to use. + - name: CALICO_NETWORKING_BACKEND + valueFrom: + configMapKeyRef: + name: calico-config + key: calico_backend + # Cluster type to identify the deployment type + - name: CLUSTER_TYPE + value: "k8s,bgp" + # Auto-detect the BGP IP address. + - name: IP + value: "autodetect" + # Enable IPIP + - name: CALICO_IPV4POOL_IPIP + value: "Always" + # Set MTU for tunnel device used if ipip is enabled + - name: FELIX_IPINIPMTU + valueFrom: + configMapKeyRef: + name: calico-config + key: veth_mtu + # The default IPv4 pool to create on startup if none exists. Pod IPs will be + # chosen from this range. Changing this value after installation will have + # no effect. This should fall within `--cluster-cidr`. + - name: CALICO_IPV4POOL_CIDR + value: "192.168.0.0/16" + # Disable file logging so `kubectl logs` works. + - name: CALICO_DISABLE_FILE_LOGGING + value: "true" + # Set Felix endpoint to host default action to ACCEPT. + - name: FELIX_DEFAULTENDPOINTTOHOSTACTION + value: "ACCEPT" + # Disable IPv6 on Kubernetes. + - name: FELIX_IPV6SUPPORT + value: "false" + # Set Felix logging to "info" + - name: FELIX_LOGSEVERITYSCREEN + value: "info" + - name: FELIX_HEALTHENABLED + value: "true" + securityContext: + privileged: true + resources: + requests: + cpu: 250m + livenessProbe: + httpGet: + path: /liveness + port: 9099 + host: localhost + periodSeconds: 10 + initialDelaySeconds: 10 + failureThreshold: 6 + readinessProbe: + exec: + command: + - /bin/calico-node + - -bird-ready + - -felix-ready + periodSeconds: 10 + volumeMounts: + - mountPath: /lib/modules + name: lib-modules + readOnly: true + - mountPath: /run/xtables.lock + name: xtables-lock + readOnly: false + - mountPath: /var/run/calico + name: var-run-calico + readOnly: false + - mountPath: /var/lib/calico + name: var-lib-calico + readOnly: false + - name: policysync + mountPath: /var/run/nodeagent + volumes: + # Used by calico-node. + - name: lib-modules + hostPath: + path: /lib/modules + - name: var-run-calico + hostPath: + path: /var/run/calico + - name: var-lib-calico + hostPath: + path: /var/lib/calico + - name: xtables-lock + hostPath: + path: /run/xtables.lock + type: FileOrCreate + # Used to install CNI. + - name: cni-bin-dir + hostPath: + path: /opt/cni/bin + - name: cni-net-dir + hostPath: + path: /etc/cni/net.d + # Mount in the directory for host-local IPAM allocations. This is + # used when upgrading from host-local to calico-ipam, and can be removed + # if not using the upgrade-ipam init container. + - name: host-local-net-dir + hostPath: + path: /var/lib/cni/networks + # Used to create per-pod Unix Domain Sockets + - name: policysync + hostPath: + type: DirectoryOrCreate + path: /var/run/nodeagent + # Used to install Flex Volume Driver + - name: flexvol-driver-host + hostPath: + type: DirectoryOrCreate + path: /usr/libexec/kubernetes/kubelet-plugins/volume/exec/nodeagent~uds +--- + +apiVersion: v1 +kind: ServiceAccount +metadata: + name: calico-node + namespace: kube-system + +--- +# Source: calico/templates/calico-kube-controllers.yaml + +# See https://github.com/projectcalico/kube-controllers +apiVersion: apps/v1 +kind: Deployment +metadata: + name: calico-kube-controllers + namespace: kube-system + labels: + k8s-app: calico-kube-controllers +spec: + # The controllers can only have a single active instance. + replicas: 1 + selector: + matchLabels: + k8s-app: calico-kube-controllers + strategy: + type: Recreate + template: + metadata: + name: calico-kube-controllers + namespace: kube-system + labels: + k8s-app: calico-kube-controllers + annotations: + scheduler.alpha.kubernetes.io/critical-pod: '' + spec: + nodeSelector: + beta.kubernetes.io/os: linux + tolerations: + # Mark the pod as a critical add-on for rescheduling. + - key: CriticalAddonsOnly + operator: Exists + - key: node-role.kubernetes.io/master + effect: NoSchedule + serviceAccountName: calico-kube-controllers + priorityClassName: system-cluster-critical + containers: + - name: calico-kube-controllers + image: calico/kube-controllers:v3.8.2 + env: + # Choose which controllers to run. + - name: ENABLED_CONTROLLERS + value: node + - name: DATASTORE_TYPE + value: kubernetes + readinessProbe: + exec: + command: + - /usr/bin/check-status + - -r + +--- + +apiVersion: v1 +kind: ServiceAccount +metadata: + name: calico-kube-controllers + namespace: kube-system +--- +# Source: calico/templates/calico-etcd-secrets.yaml + +--- +# Source: calico/templates/calico-typha.yaml + +--- +# Source: calico/templates/configure-canal.yaml + diff --git a/examples/cluster/cluster.yaml b/examples/cluster/cluster.yaml new file mode 100644 index 00000000..ac66f9d1 --- /dev/null +++ b/examples/cluster/cluster.yaml @@ -0,0 +1,29 @@ +--- +apiVersion: cluster.x-k8s.io/v1alpha3 +kind: Cluster +metadata: + name: ${CLUSTER_NAME} +spec: + clusterNetwork: + services: + cidrBlocks: ["10.96.0.0/12"] + pods: + cidrBlocks: ["192.168.0.0/16"] + serviceDomain: "cluster.local" + infrastructureRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 + kind: Metal3Cluster + name: ${CLUSTER_NAME} + controlPlaneRef: + kind: KubeadmControlPlane + apiVersion: controlplane.cluster.x-k8s.io/v1alpha3 + name: ${CLUSTER_NAME}-controlplane +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 +kind: Metal3Cluster +metadata: + name: ${CLUSTER_NAME} +spec: + controlPlaneEndpoint: + host: 192.168.111.249 + port: 6443 diff --git a/examples/cluster/kustomization.yaml b/examples/cluster/kustomization.yaml new file mode 100644 index 00000000..ee4770ad --- /dev/null +++ b/examples/cluster/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: default +resources: +- cluster.yaml +configurations: +- kustomizeconfig.yaml diff --git a/examples/cluster/kustomizeconfig.yaml b/examples/cluster/kustomizeconfig.yaml new file mode 100644 index 00000000..008398b4 --- /dev/null +++ b/examples/cluster/kustomizeconfig.yaml @@ -0,0 +1,6 @@ +namespace: +- kind: Cluster + group: cluster.x-k8s.io + version: v1alpha3 + path: spec/infrastructureRef/namespace + create: true diff --git a/examples/clusterctl-templates/clusterctl-cluster.yaml b/examples/clusterctl-templates/clusterctl-cluster.yaml new file mode 100644 index 00000000..809d7345 --- /dev/null +++ b/examples/clusterctl-templates/clusterctl-cluster.yaml @@ -0,0 +1,128 @@ +apiVersion: cluster.x-k8s.io/v1alpha3 +kind: Cluster +metadata: + name: ${ CLUSTER_NAME } + namespace: ${ NAMESPACE } +spec: + clusterNetwork: + services: + cidrBlocks: [${ SERVICE_CIDR }] + pods: + cidrBlocks: [${ POD_CIDR }] + infrastructureRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 + kind: Metal3Cluster + name: ${ CLUSTER_NAME } + controlPlaneRef: + kind: KubeadmControlPlane + apiVersion: controlplane.cluster.x-k8s.io/v1alpha3 + name: ${ CLUSTER_NAME } +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 +kind: Metal3Cluster +metadata: + name: ${ CLUSTER_NAME } + namespace: ${ NAMESPACE } +spec: + controlPlaneEndpoint: + host: ${ API_ENDPOINT_HOST } + port: ${ API_ENDPOINT_PORT } + noCloudProvider: true +--- +kind: KubeadmControlPlane +apiVersion: controlplane.cluster.x-k8s.io/v1alpha3 +metadata: + name: ${ CLUSTER_NAME } + namespace: ${ NAMESPACE } +spec: + replicas: ${ CONTROL_PLANE_MACHINE_COUNT } + version: ${ KUBERNETES_VERSION } + infrastructureTemplate: + kind: Metal3MachineTemplate + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 + name: ${ CLUSTER_NAME }-controlplane + kubeadmConfigSpec: + joinConfiguration: + controlPlane: {} + nodeRegistration: + name: '{{ ds.meta_data.name }}' + kubeletExtraArgs: + node-labels: 'metal3.io/uuid={{ ds.meta_data.uuid }}' + initConfiguration: + nodeRegistration: + name: '{{ ds.meta_data.name }}' + kubeletExtraArgs: + node-labels: 'metal3.io/uuid={{ ds.meta_data.uuid }}' +${ CTLPLANE_KUBEADM_EXTRA_CONFIG } +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 +kind: Metal3MachineTemplate +metadata: + name: ${ CLUSTER_NAME }-controlplane + namespace: ${ NAMESPACE } +spec: + template: + spec: + image: + url: ${ IMAGE_URL } + checksum: ${ IMAGE_CHECKSUM } +--- +apiVersion: cluster.x-k8s.io/v1alpha3 +kind: MachineDeployment +metadata: + name: ${ CLUSTER_NAME } + namespace: ${ NAMESPACE } + labels: + cluster.x-k8s.io/cluster-name: ${ CLUSTER_NAME } + nodepool: nodepool-0 +spec: + clusterName: ${ CLUSTER_NAME } + replicas: ${ WORKER_MACHINE_COUNT } + selector: + matchLabels: + cluster.x-k8s.io/cluster-name: ${ CLUSTER_NAME } + nodepool: nodepool-0 + template: + metadata: + labels: + cluster.x-k8s.io/cluster-name: ${ CLUSTER_NAME } + nodepool: nodepool-0 + spec: + clusterName: ${ CLUSTER_NAME } + version: ${ KUBERNETES_VERSION } + bootstrap: + configRef: + name: ${ CLUSTER_NAME }-workers + apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3 + kind: KubeadmConfigTemplate + infrastructureRef: + name: ${ CLUSTER_NAME }-workers + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 + kind: Metal3MachineTemplate +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 +kind: Metal3MachineTemplate +metadata: + name: ${ CLUSTER_NAME }-workers + namespace: ${ NAMESPACE } +spec: + template: + spec: + image: + url: ${ IMAGE_URL } + checksum: ${ IMAGE_CHECKSUM } +--- +apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3 +kind: KubeadmConfigTemplate +metadata: + name: ${ CLUSTER_NAME }-workers + namespace: ${ NAMESPACE } +spec: + template: + spec: + joinConfiguration: + nodeRegistration: + name: '{{ ds.meta_data.name }}' + kubeletExtraArgs: + node-labels: 'metal3.io/uuid={{ ds.meta_data.uuid }}' +${ WORKERS_KUBEADM_EXTRA_CONFIG } diff --git a/examples/clusterctl-templates/example_variables.rc b/examples/clusterctl-templates/example_variables.rc new file mode 100644 index 00000000..19a45fb2 --- /dev/null +++ b/examples/clusterctl-templates/example_variables.rc @@ -0,0 +1,105 @@ +export POD_CIDR='"192.168.0.0/24"' +export SERVICE_CIDR='"10.96.0.0/12"' +export API_ENDPOINT_HOST="192.168.111.249" +export API_ENDPOINT_PORT="6443" +export IMAGE_URL="http://192.168.0.1/ubuntu.qcow2" +export IMAGE_CHECKSUM="http://192.168.0.1/ubuntu.qcow2.md5sum" +export CTLPLANE_KUBEADM_EXTRA_CONFIG=" + preKubeadmCommands: + - ip link set dev enp2s0 up + - dhclient enp2s0 + - apt update -y + - netplan apply + - >- + apt install net-tools gcc linux-headers-$(uname -r) bridge-utils + apt-transport-https ca-certificates curl gnupg-agent + software-properties-common -y + - apt install -y keepalived && systemctl stop keepalived + - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - + - add-apt-repository \"deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\" + - curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - + - echo 'deb https://apt.kubernetes.io/ kubernetes-xenial main' > /etc/apt/sources.list.d/kubernetes.list + - apt update -y + - apt install docker-ce docker-ce-cli containerd.io kubelet kubeadm kubectl -y + - systemctl enable --now docker kubelet + - if (curl -sk --max-time 10 https://{{ CLUSTER_APIENDPOINT_HOST }}:6443/healthz); then echo \"keepalived already running\";else systemctl start keepalived; fi + - usermod -aG docker ubuntu + postKubeadmCommands: + - mkdir -p /home/ubuntu/.kube + - cp /etc/kubernetes/admin.conf /home/ubuntu/.kube/config + - systemctl enable --now keepalived + - chown ubuntu:ubuntu /home/ubuntu/.kube/config + files: + - path: /etc/keepalived/keepalived.conf + content: | + ! Configuration File for keepalived + global_defs { + notification_email { + sysadmin@example.com + support@example.com + } + notification_email_from lb@example.com + smtp_server localhost + smtp_connect_timeout 30 + } + vrrp_instance VI_2 { + state MASTER + interface enp2s0 + virtual_router_id 2 + priority 101 + advert_int 1 + virtual_ipaddress { + {{ CLUSTER_APIENDPOINT_HOST }} + } + } + - path: /etc/netplan/50-cloud-init.yaml + owner: root:root + permissions: '0644' + content: | + network: + ethernets: + enp2s0: + dhcp4: true + version: 2 + - path : /etc/netplan/60-ironicendpoint.yaml + owner: root:root + permissions: '0644' + content: | + network: + version: 2 + renderer: networkd + bridges: + ironicendpoint: + interfaces: [enp1s0] + dhcp4: yes +" +export WORKERS_KUBEADM_EXTRA_CONFIG=" + preKubeadmCommands: + - ip link set dev enp2s0 up + - dhclient enp2s0 + - apt update -y + - netplan apply + - >- + apt install apt-transport-https ca-certificates + curl gnupg-agent software-properties-common -y + - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - + - add-apt-repository \"deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\" + - curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - + - echo 'deb https://apt.kubernetes.io/ kubernetes-xenial main' > /etc/apt/sources.list.d/kubernetes.list + - apt update -y + - apt install docker-ce docker-ce-cli containerd.io kubelet kubeadm kubectl -y + - systemctl enable --now docker kubelet + - usermod -aG docker ubuntu + files: + - path: /etc/netplan/50-cloud-init.yaml + owner: root:root + permissions: '0644' + content: | + network: + ethernets: + enp1s0: + dhcp4: true + enp2s0: + dhcp4: true + version: 2 +" diff --git a/examples/controlplane/controlplane.yaml b/examples/controlplane/controlplane.yaml new file mode 100644 index 00000000..2e3d6d24 --- /dev/null +++ b/examples/controlplane/controlplane.yaml @@ -0,0 +1,180 @@ +kind: KubeadmControlPlane +apiVersion: controlplane.cluster.x-k8s.io/v1alpha3 +metadata: + name: ${CLUSTER_NAME}-controlplane +spec: + replicas: 3 + version: v1.17.0 + infrastructureTemplate: + kind: Metal3MachineTemplate + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 + name: ${CLUSTER_NAME}-controlplane + kubeadmConfigSpec: + initConfiguration: + nodeRegistration: + name: '{{ ds.meta_data.local_hostname }}' + kubeletExtraArgs: + cloud-provider: baremetal + clusterConfiguration: + apiServer: + extraArgs: + cloud-provider: baremetal + controllerManager: + extraArgs: + cloud-provider: baremetal + joinConfiguration: + controlPlane: {} + nodeRegistration: + name: '{{ ds.meta_data.local_hostname }}' + kubeletExtraArgs: + cloud-provider: baremetal +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 +kind: Metal3MachineTemplate +metadata: + name: ${CLUSTER_NAME}-controlplane +spec: + template: + spec: + image: + url: "http://172.22.0.1/images/rhcos-ootpa-latest.qcow2" + checksum: "97830b21ed272a3d854615beb54cf004" + dataTemplate: + name: ${CLUSTER_NAME}-cp-metadata +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 +kind: Metal3DataTemplate +metadata: + name: ${CLUSTER_NAME}-cp-metadata +spec: + clusterName: ${CLUSTER_NAME} + metaData: + strings: + - key: abc + value: def + indexes: + - key: index_0_1 + offset: 0 + step: 1 + - key: index_3 + offset: 3 + - key: index_5_2 + offset: 5 + step: 2 + objectNames: + - key: machine_name + object: machine + - key: metal3machine_name + object: metal3machine + - key: bmh_name + object: baremetalhost + ipAddressesFromPool: + - key: ip_1 + name: pool1 + - key: ip_2 + name: pool2 + - key: ip6_1 + name: pool6-1 + - key: ip6_2 + name: pool6-2 + prefixesFromPool: + - key: prefix_1 + name: pool1 + - key: prefix_2 + name: pool2 + - key: prefix6_1 + name: pool6-1 + - key: prefix6_2 + name: pool6-2 + gatewaysFromPool: + - key: gateway_1 + name: pool1 + - key: gateway_2 + name: pool2 + - key: gateway6_1 + name: pool6-1 + - key: gateway6_2 + name: pool6-2 + fromHostInterfaces: + - key: mac + interface: eth0 + networkData: + links: + ethernets: + - type: phy + id: enp1s0 + macAddress: + fromHostInterface: eth0 + - type: phy + id: enp2s0 + macAddress: + fromHostInterface: eth1 + networks: + ipv4DHCP: + - id: provisioning + link: enp1s0 + ipv4: + - id: baremetal + link: enp2s0 + ipAddressFromIPPool: pool1 + routes: + - network: 0.0.0.0 + prefix: 0 + gateway: + fromIPPool: pool1 + services: + dns: + - 8.8.4.4 + services: + dns: + - 8.8.8.8 +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 +kind: Metal3IPPool +metadata: + name: pool1 +spec: + clusterName: ${CLUSTER_NAME} + pools: + - start: 192.168.0.10 + end: 192.168.0.250 + prefix: 24 + gateway: 192.168.0.1 + namePrefix: pool1 +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 +kind: Metal3IPPool +metadata: + name: pool2 +spec: + clusterName: ${CLUSTER_NAME} + pools: + - subnet: 192.168.1.0/24 + prefix: 25 + gateway: 192.168.1.1 + namePrefix: pool2 +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 +kind: Metal3IPPool +metadata: + name: pool6-1 +spec: + clusterName: ${CLUSTER_NAME} + pools: + - start: 2001::10 + end: 2001::ff00 + prefix: 96 + gateway: 2001::1 + namePrefix: pool6-1 +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 +kind: Metal3IPPool +metadata: + name: pool6-2 +spec: + clusterName: ${CLUSTER_NAME} + pools: + - subnet: 2001:ABC::0/96 + prefix: 96 + gateway: 2001:ABC::1 + namePrefix: pool6-2 diff --git a/examples/controlplane/kustomization.yaml b/examples/controlplane/kustomization.yaml new file mode 100644 index 00000000..6a0c202d --- /dev/null +++ b/examples/controlplane/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: default +resources: +- controlplane.yaml +configurations: +- kustomizeconfig.yaml diff --git a/examples/controlplane/kustomizeconfig.yaml b/examples/controlplane/kustomizeconfig.yaml new file mode 100644 index 00000000..7e2a7baf --- /dev/null +++ b/examples/controlplane/kustomizeconfig.yaml @@ -0,0 +1,15 @@ +namespace: +- kind: Machine + group: cluster.x-k8s.io + version: v1alpha3 + path: spec/infrastructureRef/namespace + create: true +- kind: Machine + group: cluster.x-k8s.io + version: v1alpha3 + path: spec/bootstrap/configRef/namespace + create: true + +commonLabels: +- path: metadata/labels + create: true diff --git a/examples/generate.sh b/examples/generate.sh new file mode 100755 index 00000000..7ff8a064 --- /dev/null +++ b/examples/generate.sh @@ -0,0 +1,128 @@ +#!/bin/bash +# Copyright 2019 The Kubernetes 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. + +set -o errexit +set -o nounset + +# Directories. +SOURCE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" +OUTPUT_DIR=${OUTPUT_DIR:-${SOURCE_DIR}/_out} + +# Binaries +ENVSUBST=${ENVSUBST:-envsubst} +command -v "${ENVSUBST}" >/dev/null 2>&1 || echo -v "Cannot find ${ENVSUBST} in path." + +# Cluster. +export CLUSTER_NAME="${CLUSTER_NAME:-test1}" +export KUBERNETES_VERSION="${KUBERNETES_VERSION:-v1.16.0}" + +# Machine settings. +export CONTROL_PLANE_MACHINE_TYPE="${CONTROL_PLANE_MACHINE_TYPE:-t2.medium}" +export NODE_MACHINE_TYPE="${CONTROL_PLANE_MACHINE_TYPE:-t2.medium}" +export SSH_KEY_NAME="${SSH_KEY_NAME:-default}" + +# Outputs. +COMPONENTS_CERT_MANAGER_GENERATED_FILE=${OUTPUT_DIR}/cert-manager.yaml +COMPONENTS_CLUSTER_API_GENERATED_FILE=${SOURCE_DIR}/provider-components/core-components.yaml +COMPONENTS_KUBEADM_GENERATED_FILE=${SOURCE_DIR}/provider-components/bootstrap-components.yaml +COMPONENTS_CTRLPLANE_GENERATED_FILE=${SOURCE_DIR}/provider-components/ctlplane-components.yaml +COMPONENTS_METAL3_GENERATED_FILE=${SOURCE_DIR}/provider-components/infrastructure-components.yaml + +PROVIDER_COMPONENTS_GENERATED_FILE=${OUTPUT_DIR}/provider-components.yaml +CLUSTER_GENERATED_FILE=${OUTPUT_DIR}/cluster.yaml +CONTROLPLANE_GENERATED_FILE=${OUTPUT_DIR}/controlplane.yaml +MACHINEDEPLOYMENT_GENERATED_FILE=${OUTPUT_DIR}/machinedeployment.yaml +METAL3PLANE_GENERATED_FILE=${OUTPUT_DIR}/metal3plane.yaml +METAL3CRDS_GENERATED_FILE=${OUTPUT_DIR}/metal3crds.yaml + +# Overwrite flag. +OVERWRITE=0 + +SCRIPT=$(basename "$0") +while test $# -gt 0; do + case "$1" in + -h|--help) + echo "$SCRIPT - generates input yaml files for Cluster API on metal3" + echo " " + echo "$SCRIPT [options]" + echo " " + echo "options:" + echo "-h, --help show brief help" + echo "-f, --force-overwrite if file to be generated already exists, force script to overwrite it" + exit 0 + ;; + -f) + OVERWRITE=1 + shift + ;; + --force-overwrite) + OVERWRITE=1 + shift + ;; + *) + break + ;; + esac +done + +if [ $OVERWRITE -ne 1 ] && [ -d "$OUTPUT_DIR" ]; then + echo "ERR: Folder ${OUTPUT_DIR} already exists. Delete it manually before running this script." + exit 1 +fi + +mkdir -p "${OUTPUT_DIR}" + +# Generate cluster resources. +kustomize build "${SOURCE_DIR}/cluster" | envsubst > "${CLUSTER_GENERATED_FILE}" +echo "Generated ${CLUSTER_GENERATED_FILE}" + +# Generate controlplane resources. +kustomize build "${SOURCE_DIR}/controlplane" | envsubst > "${CONTROLPLANE_GENERATED_FILE}" +echo "Generated ${CONTROLPLANE_GENERATED_FILE}" + +# Generate metal3crds resources. +kustomize build "${SOURCE_DIR}/metal3crds" | envsubst > "${METAL3CRDS_GENERATED_FILE}" +echo "Generated ${METAL3CRDS_GENERATED_FILE}" + +# Generate metal3plane resources. +kustomize build "${SOURCE_DIR}/metal3plane" | envsubst > "${METAL3PLANE_GENERATED_FILE}" +echo "Generated ${METAL3PLANE_GENERATED_FILE}" + +# Generate machinedeployment resources. +kustomize build "${SOURCE_DIR}/machinedeployment" | envsubst >> "${MACHINEDEPLOYMENT_GENERATED_FILE}" +echo "Generated ${MACHINEDEPLOYMENT_GENERATED_FILE}" + +# Get Cert-manager provider components file +curl -L -o "${COMPONENTS_CERT_MANAGER_GENERATED_FILE}" https://github.com/jetstack/cert-manager/releases/download/v0.13.0/cert-manager.yaml + +# Generate Cluster API provider components file. +kustomize build "github.com/kubernetes-sigs/cluster-api/config/?ref=master" > "${COMPONENTS_CLUSTER_API_GENERATED_FILE}" +echo "Generated ${COMPONENTS_CLUSTER_API_GENERATED_FILE}" + +# Generate Kubeadm Bootstrap Provider components file. +kustomize build "github.com/kubernetes-sigs/cluster-api/bootstrap/kubeadm/config/?ref=master" > "${COMPONENTS_KUBEADM_GENERATED_FILE}" +echo "Generated ${COMPONENTS_KUBEADM_GENERATED_FILE}" + +# Generate Kubeadm Controlplane components file. +kustomize build "github.com/kubernetes-sigs/cluster-api/controlplane/kubeadm/config/?ref=master" > "${COMPONENTS_CTRLPLANE_GENERATED_FILE}" +echo "Generated ${COMPONENTS_CTRLPLANE_GENERATED_FILE}" + +# Generate METAL3 Infrastructure Provider components file. +kustomize build "${SOURCE_DIR}/../config" | envsubst > "${COMPONENTS_METAL3_GENERATED_FILE}" +echo "Generated ${COMPONENTS_METAL3_GENERATED_FILE}" + +# Generate a single provider components file. +kustomize build "${SOURCE_DIR}/provider-components" | envsubst > "${PROVIDER_COMPONENTS_GENERATED_FILE}" +echo "Generated ${PROVIDER_COMPONENTS_GENERATED_FILE}" diff --git a/examples/machinedeployment/kustomization.yaml b/examples/machinedeployment/kustomization.yaml new file mode 100644 index 00000000..b3c0eecf --- /dev/null +++ b/examples/machinedeployment/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: default +resources: +- machinedeployment.yaml +configurations: +- kustomizeconfig.yaml diff --git a/examples/machinedeployment/kustomizeconfig.yaml b/examples/machinedeployment/kustomizeconfig.yaml new file mode 100644 index 00000000..e0e40917 --- /dev/null +++ b/examples/machinedeployment/kustomizeconfig.yaml @@ -0,0 +1,11 @@ +namespace: +- kind: MachineDeployment + group: cluster.x-k8s.io + version: v1alpha3 + path: spec/template/spec/infrastructureRef/namespace + create: true +- kind: MachineDeployment + group: cluster.x-k8s.io + version: v1alpha3 + path: spec/template/spec/bootstrap/configRef/namespace + create: true diff --git a/examples/machinedeployment/machinedeployment.yaml b/examples/machinedeployment/machinedeployment.yaml new file mode 100644 index 00000000..8dfcc2d4 --- /dev/null +++ b/examples/machinedeployment/machinedeployment.yaml @@ -0,0 +1,144 @@ +apiVersion: cluster.x-k8s.io/v1alpha3 +kind: MachineDeployment +metadata: + name: ${CLUSTER_NAME}-md-0 + labels: + cluster.x-k8s.io/cluster-name: ${CLUSTER_NAME} + nodepool: nodepool-0 +spec: + clusterName: ${CLUSTER_NAME} + replicas: 2 + selector: + matchLabels: + cluster.x-k8s.io/cluster-name: ${CLUSTER_NAME} + nodepool: nodepool-0 + template: + metadata: + labels: + cluster.x-k8s.io/cluster-name: ${CLUSTER_NAME} + nodepool: nodepool-0 + spec: + clusterName: ${CLUSTER_NAME} + version: ${KUBERNETES_VERSION} + bootstrap: + configRef: + name: ${CLUSTER_NAME}-md-0 + apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3 + kind: KubeadmConfigTemplate + infrastructureRef: + name: ${CLUSTER_NAME}-md-0 + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 + kind: Metal3MachineTemplate +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 +kind: Metal3MachineTemplate +metadata: + name: ${CLUSTER_NAME}-md-0 +spec: + template: + spec: + image: + url: "http://172.22.0.1/images/rhcos-ootpa-latest.qcow2" + checksum: "97830b21ed272a3d854615beb54cf004" + dataTemplate: + name: ${CLUSTER_NAME}-md-metadata +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 +kind: Metal3DataTemplate +metadata: + name: ${CLUSTER_NAME}-md-metadata +spec: + clusterName: ${CLUSTER_NAME} + metaData: + strings: + - key: abc + value: def + indexes: + - key: index_0_1 + offset: 0 + step: 1 + - key: index_3 + offset: 3 + - key: index_5_2 + offset: 5 + step: 2 + objectNames: + - key: machine_name + object: machine + - key: metal3machine_name + object: metal3machine + - key: bmh_name + object: baremetalhost + ipAddressesFromPool: + - key: ip_1 + name: pool1 + - key: ip_2 + name: pool2 + - key: ip6_1 + name: pool6-1 + - key: ip6_2 + name: pool6-2 + prefixesFromPool: + - key: prefix_1 + name: pool1 + - key: prefix_2 + name: pool2 + - key: prefix6_1 + name: pool6-1 + - key: prefix6_2 + name: pool6-2 + gatewaysFromPool: + - key: gateway_1 + name: pool1 + - key: gateway_2 + name: pool2 + - key: gateway6_1 + name: pool6-1 + - key: gateway6_2 + name: pool6-2 + fromHostInterfaces: + - key: mac + interface: eth0 + networkData: + links: + ethernets: + - type: phy + id: enp1s0 + macAddress: + fromHostInterface: eth0 + - type: phy + id: enp2s0 + macAddress: + fromHostInterface: eth1 + networks: + ipv4DHCP: + - id: provisioning + link: enp1s0 + ipv4: + - id: baremetal + link: enp2s0 + ipAddressFromIPPool: pool1 + routes: + - network: 0.0.0.0 + prefix: 0 + gateway: + fromIPPool: pool1 + services: + dns: + - 8.8.4.4 + services: + dns: + - 8.8.8.8 +--- +apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3 +kind: KubeadmConfigTemplate +metadata: + name: ${CLUSTER_NAME}-md-0 +spec: + template: + spec: + joinConfiguration: + nodeRegistration: + name: '{{ ds.meta_data.hostname }}' + kubeletExtraArgs: + cloud-provider: baremetal diff --git a/examples/metal3crds/kustomization.yaml b/examples/metal3crds/kustomization.yaml new file mode 100644 index 00000000..91d70fdb --- /dev/null +++ b/examples/metal3crds/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: default +resources: +- metal3.io_baremetalhosts.yaml diff --git a/examples/metal3crds/metal3.io_baremetalhosts.yaml b/examples/metal3crds/metal3.io_baremetalhosts.yaml new file mode 100644 index 00000000..f69efcad --- /dev/null +++ b/examples/metal3crds/metal3.io_baremetalhosts.yaml @@ -0,0 +1,574 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: baremetalhosts.metal3.io +spec: + additionalPrinterColumns: + - JSONPath: .status.operationalStatus + description: Operational status + name: Status + type: string + - JSONPath: .status.provisioning.state + description: Provisioning status + name: Provisioning Status + type: string + - JSONPath: .spec.consumerRef.name + description: Consumer using this host + name: Consumer + type: string + - JSONPath: .spec.bmc.address + description: Address of management controller + name: BMC + type: string + - JSONPath: .status.hardwareProfile + description: The type of hardware detected + name: Hardware Profile + type: string + - JSONPath: .spec.online + description: Whether the host is online or not + name: Online + type: string + - JSONPath: .status.errorMessage + description: Most recent error + name: Error + type: string + group: metal3.io + names: + kind: BareMetalHost + listKind: BareMetalHostList + plural: baremetalhosts + shortNames: + - bmh + - bmhost + singular: baremetalhost + scope: Namespaced + validation: + openAPIV3Schema: + description: BareMetalHost is the Schema for the baremetalhosts API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: BareMetalHostSpec defines the desired state of BareMetalHost + properties: + bmc: + description: How do we connect to the BMC? + properties: + address: + description: Address holds the URL for accessing the controller + on the network. + type: string + credentialsName: + description: The name of the secret containing the BMC credentials + (requires keys "username" and "password"). + type: string + disableCertificateVerification: + description: DisableCertificateVerification disables verification + of server certificates when using HTTPS to connect to the BMC. + This is required when the server certificate is self-signed, but + is insecure because it allows a man-in-the-middle to intercept + the connection. + type: boolean + required: + - address + - credentialsName + type: object + bootMACAddress: + description: Which MAC address will PXE boot? This is optional for some + types, but required for libvirt VMs driven by vbmc. + pattern: '[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}' + type: string + consumerRef: + description: ConsumerRef can be used to store information about something + that is using a host. When it is not empty, the host is considered + "in use". + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of an + entire object, this string should contain a valid JSON/Go field + access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen only + to have some well-defined way of referencing a part of an object. + TODO: this design is not final and this field is subject to change + in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference is + made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + description: + description: Description is a human-entered text used to help identify + the host + type: string + externallyProvisioned: + description: ExternallyProvisioned means something else is managing + the image running on the host and the operator should only manage + the power status and hardware inventory inspection. If the Image field + is filled in, this field is ignored. + type: boolean + hardwareProfile: + description: What is the name of the hardware profile for this host? + It should only be necessary to set this when inspection cannot automatically + determine the profile. + type: string + image: + description: Image holds the details of the image to be provisioned. + properties: + checksum: + description: Checksum is the checksum for the image. + type: string + url: + description: URL is a location of an image to deploy. + type: string + required: + - checksum + - url + type: object + metaData: + description: MetaData holds the reference to the Secret containing host + metadata (e.g. meta_data.json which is passed to Config Drive). + properties: + name: + description: Name is unique within a namespace to reference a secret + resource. + type: string + namespace: + description: Namespace defines the space within which the secret + name must be unique. + type: string + type: object + networkData: + description: NetworkData holds the reference to the Secret containing + network configuration (e.g content of network_data.json which is passed + to Config Drive). + properties: + name: + description: Name is unique within a namespace to reference a secret + resource. + type: string + namespace: + description: Namespace defines the space within which the secret + name must be unique. + type: string + type: object + online: + description: Should the server be online? + type: boolean + taints: + description: Taints is the full, authoritative list of taints to apply + to the corresponding Machine. This list will overwrite any modifications + made to the Machine on an ongoing basis. + items: + description: The node this Taint is attached to has the "effect" on + any pod that does not tolerate the Taint. + properties: + effect: + description: Required. The effect of the taint on pods that do + not tolerate the taint. Valid effects are NoSchedule, PreferNoSchedule + and NoExecute. + type: string + key: + description: Required. The taint key to be applied to a node. + type: string + timeAdded: + description: TimeAdded represents the time at which the taint + was added. It is only written for NoExecute taints. + format: date-time + type: string + value: + description: Required. The taint value corresponding to the taint + key. + type: string + required: + - effect + - key + type: object + type: array + userData: + description: UserData holds the reference to the Secret containing the + user data to be passed to the host before it boots. + properties: + name: + description: Name is unique within a namespace to reference a secret + resource. + type: string + namespace: + description: Namespace defines the space within which the secret + name must be unique. + type: string + type: object + required: + - online + type: object + status: + description: BareMetalHostStatus defines the observed state of BareMetalHost + properties: + errorMessage: + description: the last error message reported by the provisioning subsystem + type: string + errorType: + description: ErrorType indicates the type of failure encountered when + the OperationalStatus is OperationalStatusError + enum: + - registration error + - inspection error + - provisioning error + - power management error + type: string + goodCredentials: + description: the last credentials we were able to validate as working + properties: + credentials: + description: SecretReference represents a Secret Reference. It has + enough information to retrieve secret in any namespace + properties: + name: + description: Name is unique within a namespace to reference + a secret resource. + type: string + namespace: + description: Namespace defines the space within which the secret + name must be unique. + type: string + type: object + credentialsVersion: + type: string + type: object + hardware: + description: The hardware discovered to exist on the host. + properties: + cpu: + description: CPU describes one processor on the host. + properties: + arch: + type: string + clockMegahertz: + description: ClockSpeed is a clock speed in MHz + count: + type: integer + flags: + items: + type: string + type: array + model: + type: string + required: + - arch + - clockMegahertz + - count + - flags + - model + type: object + firmware: + description: Firmware describes the firmware on the host. + properties: + bios: + description: The BIOS for this firmware + properties: + date: + description: The release/build date for this BIOS + type: string + vendor: + description: The vendor name for this BIOS + type: string + version: + description: The version of the BIOS + type: string + required: + - date + - vendor + - version + type: object + required: + - bios + type: object + hostname: + type: string + nics: + items: + description: NIC describes one network interface on the host. + properties: + ip: + description: The IP address of the device + type: string + mac: + description: The device MAC addr + pattern: '[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}' + type: string + model: + description: The name of the model, e.g. "virt-io" + type: string + name: + description: The name of the NIC, e.g. "nic-1" + type: string + pxe: + description: Whether the NIC is PXE Bootable + type: boolean + speedGbps: + description: The speed of the device + type: integer + vlanId: + description: The untagged VLAN ID + format: int32 + type: integer + vlans: + description: The VLANs available + items: + description: VLAN represents the name and ID of a VLAN + properties: + id: + description: VLANID is a 12-bit 802.1Q VLAN identifier + format: int32 + type: integer + name: + type: string + required: + - id + type: object + type: array + required: + - ip + - mac + - model + - name + - pxe + - speedGbps + - vlanId + type: object + type: array + ramMebibytes: + type: integer + storage: + items: + description: Storage describes one storage device (disk, SSD, + etc.) on the host. + properties: + hctl: + description: The SCSI location of the device + type: string + model: + description: Hardware model + type: string + name: + description: A name for the disk, e.g. "disk 1 (boot)" + type: string + rotational: + description: Whether this disk represents rotational storage + type: boolean + serialNumber: + description: The serial number of the device + type: string + sizeBytes: + description: The size of the disk in Bytes + format: int64 + type: integer + vendor: + description: The name of the vendor of the device + type: string + wwn: + description: The WWN of the device + type: string + wwnVendorExtension: + description: The WWN Vendor extension of the device + type: string + wwnWithExtension: + description: The WWN with the extension + type: string + required: + - name + - rotational + - serialNumber + - sizeBytes + type: object + type: array + systemVendor: + description: HardwareSystemVendor stores details about the whole + hardware system. + properties: + manufacturer: + type: string + productName: + type: string + serialNumber: + type: string + required: + - manufacturer + - productName + - serialNumber + type: object + required: + - cpu + - firmware + - hostname + - nics + - ramMebibytes + - storage + - systemVendor + type: object + hardwareProfile: + description: The name of the profile matching the hardware details. + type: string + lastUpdated: + description: LastUpdated identifies when this status was last observed. + format: date-time + type: string + operationHistory: + description: OperationHistory holds information about operations performed + on this host. + properties: + deprovision: + description: OperationMetric contains metadata about an operation + (inspection, provisioning, etc.) used for tracking metrics. + properties: + end: + format: date-time + nullable: true + type: string + start: + format: date-time + nullable: true + type: string + type: object + inspect: + description: OperationMetric contains metadata about an operation + (inspection, provisioning, etc.) used for tracking metrics. + properties: + end: + format: date-time + nullable: true + type: string + start: + format: date-time + nullable: true + type: string + type: object + provision: + description: OperationMetric contains metadata about an operation + (inspection, provisioning, etc.) used for tracking metrics. + properties: + end: + format: date-time + nullable: true + type: string + start: + format: date-time + nullable: true + type: string + type: object + register: + description: OperationMetric contains metadata about an operation + (inspection, provisioning, etc.) used for tracking metrics. + properties: + end: + format: date-time + nullable: true + type: string + start: + format: date-time + nullable: true + type: string + type: object + type: object + operationalStatus: + description: OperationalStatus holds the status of the host + enum: + - "" + - OK + - discovered + - error + type: string + poweredOn: + description: indicator for whether or not the host is powered on + type: boolean + provisioning: + description: Information tracked by the provisioner. + properties: + ID: + description: The machine's UUID from the underlying provisioning + tool + type: string + image: + description: Image holds the details of the last image successfully + provisioned to the host. + properties: + checksum: + description: Checksum is the checksum for the image. + type: string + url: + description: URL is a location of an image to deploy. + type: string + required: + - checksum + - url + type: object + state: + description: An indiciator for what the provisioner is doing with + the host. + type: string + required: + - ID + - state + type: object + triedCredentials: + description: the last credentials we sent to the provisioning backend + properties: + credentials: + description: SecretReference represents a Secret Reference. It has + enough information to retrieve secret in any namespace + properties: + name: + description: Name is unique within a namespace to reference + a secret resource. + type: string + namespace: + description: Namespace defines the space within which the secret + name must be unique. + type: string + type: object + credentialsVersion: + type: string + type: object + required: + - errorMessage + - hardwareProfile + - operationHistory + - operationalStatus + - poweredOn + - provisioning + type: object + type: object + version: v1alpha1 + versions: + - name: v1alpha1 + served: true + storage: true diff --git a/examples/metal3plane/hosts.yaml b/examples/metal3plane/hosts.yaml new file mode 100644 index 00000000..3bbf8906 --- /dev/null +++ b/examples/metal3plane/hosts.yaml @@ -0,0 +1,749 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: demo-externally-provisioned-secret +type: Opaque +data: + username: YWRtaW4= + password: MWYyZDFlMmU2N2Rm +--- +apiVersion: metal3.io/v1alpha1 +kind: BareMetalHost +metadata: + name: metal3-0 +spec: + online: false + bmc: + address: ipmi://192.168.122.10:6233 + credentialsName: demo-externally-provisioned-secret +status: + errorMessage: "" + goodCredentials: + credentials: + name: demo-externally-provisioned-secret + namespace: default + credentialsVersion: "879" + hardware: + cpu: + arch: x86_64 + clockMegahertz: 2494.222 + count: 4 + flags: + - aes + model: Intel Xeon E3-12xx v2 (Ivy Bridge) + firmware: + bios: + date: 04/01/2014 + vendor: SeaBIOS + version: 1.10.2-1ubuntu1 + ramMebibytes: 8192 + storage: + - hctl: "6:0:0:0" + model: QEMU HARDDISK + name: /dev/sda + rotational: true + serialNumber: drive-scsi0-0-0-0 + sizeBytes: 53687091200 + vendor: QEMU + systemVendor: + manufacturer: QEMU + productName: Standard PC (Q35 + ICH9, 2009) + serialNumber: "" + hostname: master-0 + nics: + - ip: 172.22.0.11 + mac: 00:28:19:1f:79:4d + model: 0x1af4 0x0001 + name: eth0 + pxe: true + speedGbps: 0 + vlanId: 0 + - ip: 192.168.111.20 + mac: 00:28:19:1f:79:4f + model: 0x1af4 0x0001 + name: eth1 + pxe: false + speedGbps: 0 + vlanId: 0 + hardwareProfile: unknown + operationalStatus: OK + operationHistory: {} + poweredOn: false + triedCredentials: + credentials: + name: demo-externally-provisioned-secret + namespace: default + credentialsVersion: "879" + provisioning: + ID: be02cfb3-7b24-47c7-b9b8-d69207d86f1e + image: + checksum: http://172.22.0.1/images/centos-updated.qcow2.md5sum + url: http://172.22.0.1/images/centos-updated.qcow2 + state: ready +--- +apiVersion: metal3.io/v1alpha1 +kind: BareMetalHost +metadata: + name: metal3-1 +spec: + online: false + bmc: + address: ipmi://192.168.122.11:6233 + credentialsName: demo-externally-provisioned-secret +status: + errorMessage: "" + goodCredentials: + credentials: + name: demo-externally-provisioned-secret + namespace: default + credentialsVersion: "879" + hardware: + cpu: + arch: x86_64 + clockMegahertz: 2494.222 + count: 4 + flags: + - aes + model: Intel Xeon E3-12xx v2 (Ivy Bridge) + firmware: + bios: + date: 04/01/2014 + vendor: SeaBIOS + version: 1.10.2-1ubuntu1 + ramMebibytes: 8192 + storage: + - hctl: "6:0:0:0" + model: QEMU HARDDISK + name: /dev/sda + rotational: true + serialNumber: drive-scsi0-0-0-0 + sizeBytes: 53687091200 + vendor: QEMU + systemVendor: + manufacturer: QEMU + productName: Standard PC (Q35 + ICH9, 2009) + serialNumber: "" + hostname: master-0 + nics: + - ip: 172.22.0.11 + mac: 00:28:19:1f:79:4d + model: 0x1af4 0x0001 + name: eth0 + pxe: true + speedGbps: 0 + vlanId: 0 + - ip: 192.168.111.20 + mac: 00:28:19:1f:79:4f + model: 0x1af4 0x0001 + name: eth1 + pxe: false + speedGbps: 0 + vlanId: 0 + hardwareProfile: unknown + operationalStatus: OK + operationHistory: {} + poweredOn: false + triedCredentials: + credentials: + name: demo-externally-provisioned-secret + namespace: default + credentialsVersion: "879" + provisioning: + ID: be02cfb3-7b24-47c7-b9b8-d69207d86f10 + image: + checksum: http://172.22.0.1/images/centos-updated.qcow2.md5sum + url: http://172.22.0.1/images/centos-updated.qcow2 + state: ready +--- +apiVersion: metal3.io/v1alpha1 +kind: BareMetalHost +metadata: + name: metal3-2 +spec: + online: false + bmc: + address: ipmi://192.168.122.12:6233 + credentialsName: demo-externally-provisioned-secret +status: + errorMessage: "" + goodCredentials: + credentials: + name: demo-externally-provisioned-secret + namespace: default + credentialsVersion: "879" + hardware: + cpu: + arch: x86_64 + clockMegahertz: 2494.222 + count: 4 + flags: + - aes + model: Intel Xeon E3-12xx v2 (Ivy Bridge) + firmware: + bios: + date: 04/01/2014 + vendor: SeaBIOS + version: 1.10.2-1ubuntu1 + ramMebibytes: 8192 + storage: + - hctl: "6:0:0:0" + model: QEMU HARDDISK + name: /dev/sda + rotational: true + serialNumber: drive-scsi0-0-0-0 + sizeBytes: 53687091200 + vendor: QEMU + systemVendor: + manufacturer: QEMU + productName: Standard PC (Q35 + ICH9, 2009) + serialNumber: "" + hostname: master-0 + nics: + - ip: 172.22.0.11 + mac: 00:28:19:1f:79:4d + model: 0x1af4 0x0001 + name: eth0 + pxe: true + speedGbps: 0 + vlanId: 0 + - ip: 192.168.111.20 + mac: 00:28:19:1f:79:4f + model: 0x1af4 0x0001 + name: eth1 + pxe: false + speedGbps: 0 + vlanId: 0 + hardwareProfile: unknown + operationalStatus: OK + operationHistory: {} + poweredOn: false + triedCredentials: + credentials: + name: demo-externally-provisioned-secret + namespace: default + credentialsVersion: "879" + provisioning: + ID: be02cfb3-7b24-47c7-b9b8-d69207d86f11 + image: + checksum: http://172.22.0.1/images/centos-updated.qcow2.md5sum + url: http://172.22.0.1/images/centos-updated.qcow2 + state: ready +--- +apiVersion: metal3.io/v1alpha1 +kind: BareMetalHost +metadata: + name: metal3-3 +spec: + online: false + bmc: + address: ipmi://192.168.122.13:6233 + credentialsName: demo-externally-provisioned-secret +status: + errorMessage: "" + goodCredentials: + credentials: + name: demo-externally-provisioned-secret + namespace: default + credentialsVersion: "879" + hardware: + cpu: + arch: x86_64 + clockMegahertz: 2494.222 + count: 4 + flags: + - aes + model: Intel Xeon E3-12xx v2 (Ivy Bridge) + firmware: + bios: + date: 04/01/2014 + vendor: SeaBIOS + version: 1.10.2-1ubuntu1 + ramMebibytes: 8192 + storage: + - hctl: "6:0:0:0" + model: QEMU HARDDISK + name: /dev/sda + rotational: true + serialNumber: drive-scsi0-0-0-0 + sizeBytes: 53687091200 + vendor: QEMU + systemVendor: + manufacturer: QEMU + productName: Standard PC (Q35 + ICH9, 2009) + serialNumber: "" + hostname: master-0 + nics: + - ip: 172.22.0.11 + mac: 00:28:19:1f:79:4d + model: 0x1af4 0x0001 + name: eth0 + pxe: true + speedGbps: 0 + vlanId: 0 + - ip: 192.168.111.20 + mac: 00:28:19:1f:79:4f + model: 0x1af4 0x0001 + name: eth1 + pxe: false + speedGbps: 0 + vlanId: 0 + hardwareProfile: unknown + operationalStatus: OK + operationHistory: {} + poweredOn: false + triedCredentials: + credentials: + name: demo-externally-provisioned-secret + namespace: default + credentialsVersion: "879" + provisioning: + ID: be02cfb3-7b24-47c7-b9b8-d69207d86f12 + image: + checksum: http://172.22.0.1/images/centos-updated.qcow2.md5sum + url: http://172.22.0.1/images/centos-updated.qcow2 + state: ready +--- +apiVersion: metal3.io/v1alpha1 +kind: BareMetalHost +metadata: + name: metal3-4 +spec: + online: false + bmc: + address: ipmi://192.168.122.14:6233 + credentialsName: demo-externally-provisioned-secret +status: + errorMessage: "" + goodCredentials: + credentials: + name: demo-externally-provisioned-secret + namespace: default + credentialsVersion: "879" + hardware: + cpu: + arch: x86_64 + clockMegahertz: 2494.222 + count: 4 + flags: + - aes + model: Intel Xeon E3-12xx v2 (Ivy Bridge) + firmware: + bios: + date: 04/01/2014 + vendor: SeaBIOS + version: 1.10.2-1ubuntu1 + ramMebibytes: 8192 + storage: + - hctl: "6:0:0:0" + model: QEMU HARDDISK + name: /dev/sda + rotational: true + serialNumber: drive-scsi0-0-0-0 + sizeBytes: 53687091200 + vendor: QEMU + systemVendor: + manufacturer: QEMU + productName: Standard PC (Q35 + ICH9, 2009) + serialNumber: "" + hostname: master-0 + nics: + - ip: 172.22.0.11 + mac: 00:28:19:1f:79:4d + model: 0x1af4 0x0001 + name: eth0 + pxe: true + speedGbps: 0 + vlanId: 0 + - ip: 192.168.111.20 + mac: 00:28:19:1f:79:4f + model: 0x1af4 0x0001 + name: eth1 + pxe: false + speedGbps: 0 + vlanId: 0 + hardwareProfile: unknown + operationalStatus: OK + operationHistory: {} + poweredOn: false + triedCredentials: + credentials: + name: demo-externally-provisioned-secret + namespace: default + credentialsVersion: "879" + provisioning: + ID: be02cfb3-7b24-47c7-b9b8-d69207d86f13 + image: + checksum: http://172.22.0.1/images/centos-updated.qcow2.md5sum + url: http://172.22.0.1/images/centos-updated.qcow2 + state: ready +--- +apiVersion: metal3.io/v1alpha1 +kind: BareMetalHost +metadata: + name: metal3-5 +spec: + online: false + bmc: + address: ipmi://192.168.122.15:6233 + credentialsName: demo-externally-provisioned-secret +status: + errorMessage: "" + goodCredentials: + credentials: + name: demo-externally-provisioned-secret + namespace: default + credentialsVersion: "879" + hardware: + cpu: + arch: x86_64 + clockMegahertz: 2494.222 + count: 4 + flags: + - aes + model: Intel Xeon E3-12xx v2 (Ivy Bridge) + firmware: + bios: + date: 04/01/2014 + vendor: SeaBIOS + version: 1.10.2-1ubuntu1 + ramMebibytes: 8192 + storage: + - hctl: "6:0:0:0" + model: QEMU HARDDISK + name: /dev/sda + rotational: true + serialNumber: drive-scsi0-0-0-0 + sizeBytes: 53687091200 + vendor: QEMU + systemVendor: + manufacturer: QEMU + productName: Standard PC (Q35 + ICH9, 2009) + serialNumber: "" + hostname: master-0 + nics: + - ip: 172.22.0.11 + mac: 00:28:19:1f:79:4d + model: 0x1af4 0x0001 + name: eth0 + pxe: true + speedGbps: 0 + vlanId: 0 + - ip: 192.168.111.20 + mac: 00:28:19:1f:79:4f + model: 0x1af4 0x0001 + name: eth1 + pxe: false + speedGbps: 0 + vlanId: 0 + hardwareProfile: unknown + operationalStatus: OK + operationHistory: {} + poweredOn: false + triedCredentials: + credentials: + name: demo-externally-provisioned-secret + namespace: default + credentialsVersion: "879" + provisioning: + ID: be02cfb3-7b24-47c7-b9b8-d69207d86f14 + image: + checksum: http://172.22.0.1/images/centos-updated.qcow2.md5sum + url: http://172.22.0.1/images/centos-updated.qcow2 + state: ready +--- +apiVersion: metal3.io/v1alpha1 +kind: BareMetalHost +metadata: + name: metal3-6 +spec: + online: false + bmc: + address: ipmi://192.168.122.16:6233 + credentialsName: demo-externally-provisioned-secret +status: + errorMessage: "" + goodCredentials: + credentials: + name: demo-externally-provisioned-secret + namespace: default + credentialsVersion: "879" + hardware: + cpu: + arch: x86_64 + clockMegahertz: 2494.222 + count: 4 + flags: + - aes + model: Intel Xeon E3-12xx v2 (Ivy Bridge) + firmware: + bios: + date: 04/01/2014 + vendor: SeaBIOS + version: 1.10.2-1ubuntu1 + ramMebibytes: 8192 + storage: + - hctl: "6:0:0:0" + model: QEMU HARDDISK + name: /dev/sda + rotational: true + serialNumber: drive-scsi0-0-0-0 + sizeBytes: 53687091200 + vendor: QEMU + systemVendor: + manufacturer: QEMU + productName: Standard PC (Q35 + ICH9, 2009) + serialNumber: "" + hostname: master-0 + nics: + - ip: 172.22.0.11 + mac: 00:28:19:1f:79:4d + model: 0x1af4 0x0001 + name: eth0 + pxe: true + speedGbps: 0 + vlanId: 0 + - ip: 192.168.111.20 + mac: 00:28:19:1f:79:4f + model: 0x1af4 0x0001 + name: eth1 + pxe: false + speedGbps: 0 + vlanId: 0 + hardwareProfile: unknown + operationalStatus: OK + operationHistory: {} + poweredOn: false + triedCredentials: + credentials: + name: demo-externally-provisioned-secret + namespace: default + credentialsVersion: "879" + provisioning: + ID: be02cfb3-7b24-47c7-b9b8-d69207d86f15 + image: + checksum: http://172.22.0.1/images/centos-updated.qcow2.md5sum + url: http://172.22.0.1/images/centos-updated.qcow2 + state: ready +--- +apiVersion: metal3.io/v1alpha1 +kind: BareMetalHost +metadata: + name: metal3-7 +spec: + online: false + bmc: + address: ipmi://192.168.122.17:6233 + credentialsName: demo-externally-provisioned-secret +status: + errorMessage: "" + goodCredentials: + credentials: + name: demo-externally-provisioned-secret + namespace: default + credentialsVersion: "879" + hardware: + cpu: + arch: x86_64 + clockMegahertz: 2494.222 + count: 4 + flags: + - aes + model: Intel Xeon E3-12xx v2 (Ivy Bridge) + firmware: + bios: + date: 04/01/2014 + vendor: SeaBIOS + version: 1.10.2-1ubuntu1 + ramMebibytes: 8192 + storage: + - hctl: "6:0:0:0" + model: QEMU HARDDISK + name: /dev/sda + rotational: true + serialNumber: drive-scsi0-0-0-0 + sizeBytes: 53687091200 + vendor: QEMU + systemVendor: + manufacturer: QEMU + productName: Standard PC (Q35 + ICH9, 2009) + serialNumber: "" + hostname: master-0 + nics: + - ip: 172.22.0.11 + mac: 00:28:19:1f:79:4d + model: 0x1af4 0x0001 + name: eth0 + pxe: true + speedGbps: 0 + vlanId: 0 + - ip: 192.168.111.20 + mac: 00:28:19:1f:79:4f + model: 0x1af4 0x0001 + name: eth1 + pxe: false + speedGbps: 0 + vlanId: 0 + hardwareProfile: unknown + operationalStatus: OK + operationHistory: {} + poweredOn: false + triedCredentials: + credentials: + name: demo-externally-provisioned-secret + namespace: default + credentialsVersion: "879" + provisioning: + ID: be02cfb3-7b24-47c7-b9b8-d69207d86f16 + image: + checksum: http://172.22.0.1/images/centos-updated.qcow2.md5sum + url: http://172.22.0.1/images/centos-updated.qcow2 + state: ready +--- +apiVersion: metal3.io/v1alpha1 +kind: BareMetalHost +metadata: + name: metal3-8 +spec: + online: false + bmc: + address: ipmi://192.168.122.18:6233 + credentialsName: demo-externally-provisioned-secret +status: + errorMessage: "" + goodCredentials: + credentials: + name: demo-externally-provisioned-secret + namespace: default + credentialsVersion: "879" + hardware: + cpu: + arch: x86_64 + clockMegahertz: 2494.222 + count: 4 + flags: + - aes + model: Intel Xeon E3-12xx v2 (Ivy Bridge) + firmware: + bios: + date: 04/01/2014 + vendor: SeaBIOS + version: 1.10.2-1ubuntu1 + ramMebibytes: 8192 + storage: + - hctl: "6:0:0:0" + model: QEMU HARDDISK + name: /dev/sda + rotational: true + serialNumber: drive-scsi0-0-0-0 + sizeBytes: 53687091200 + vendor: QEMU + systemVendor: + manufacturer: QEMU + productName: Standard PC (Q35 + ICH9, 2009) + serialNumber: "" + hostname: master-0 + nics: + - ip: 172.22.0.11 + mac: 00:28:19:1f:79:4d + model: 0x1af4 0x0001 + name: eth0 + pxe: true + speedGbps: 0 + vlanId: 0 + - ip: 192.168.111.20 + mac: 00:28:19:1f:79:4f + model: 0x1af4 0x0001 + name: eth1 + pxe: false + speedGbps: 0 + vlanId: 0 + hardwareProfile: unknown + operationalStatus: OK + operationHistory: {} + poweredOn: false + triedCredentials: + credentials: + name: demo-externally-provisioned-secret + namespace: default + credentialsVersion: "879" + provisioning: + ID: be02cfb3-7b24-47c7-b9b8-d69207d86f17 + image: + checksum: http://172.22.0.1/images/centos-updated.qcow2.md5sum + url: http://172.22.0.1/images/centos-updated.qcow2 + state: ready +--- +apiVersion: metal3.io/v1alpha1 +kind: BareMetalHost +metadata: + name: metal3-9 +spec: + online: false + bmc: + address: ipmi://192.168.122.19:6233 + credentialsName: demo-externally-provisioned-secret +status: + errorMessage: "" + goodCredentials: + credentials: + name: demo-externally-provisioned-secret + namespace: default + credentialsVersion: "879" + hardware: + cpu: + arch: x86_64 + clockMegahertz: 2494.222 + count: 4 + flags: + - aes + model: Intel Xeon E3-12xx v2 (Ivy Bridge) + firmware: + bios: + date: 04/01/2014 + vendor: SeaBIOS + version: 1.10.2-1ubuntu1 + ramMebibytes: 8192 + storage: + - hctl: "6:0:0:0" + model: QEMU HARDDISK + name: /dev/sda + rotational: true + serialNumber: drive-scsi0-0-0-0 + sizeBytes: 53687091200 + vendor: QEMU + systemVendor: + manufacturer: QEMU + productName: Standard PC (Q35 + ICH9, 2009) + serialNumber: "" + hostname: master-0 + nics: + - ip: 172.22.0.11 + mac: 00:28:19:1f:79:4d + model: 0x1af4 0x0001 + name: eth0 + pxe: true + speedGbps: 0 + vlanId: 0 + - ip: 192.168.111.20 + mac: 00:28:19:1f:79:4f + model: 0x1af4 0x0001 + name: eth1 + pxe: false + speedGbps: 0 + vlanId: 0 + hardwareProfile: unknown + operationalStatus: OK + operationHistory: {} + poweredOn: false + triedCredentials: + credentials: + name: demo-externally-provisioned-secret + namespace: default + credentialsVersion: "879" + provisioning: + ID: be02cfb3-7b24-47c7-b9b8-d69207d86f18 + image: + checksum: http://172.22.0.1/images/centos-updated.qcow2.md5sum + url: http://172.22.0.1/images/centos-updated.qcow2 + state: ready diff --git a/examples/metal3plane/kustomization.yaml b/examples/metal3plane/kustomization.yaml new file mode 100644 index 00000000..fef80721 --- /dev/null +++ b/examples/metal3plane/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: default +resources: +- hosts.yaml diff --git a/examples/provider-components/kustomization.yaml b/examples/provider-components/kustomization.yaml new file mode 100644 index 00000000..b93bb2c6 --- /dev/null +++ b/examples/provider-components/kustomization.yaml @@ -0,0 +1,9 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- core-components.yaml +- bootstrap-components.yaml +- ctlplane-components.yaml +- infrastructure-components.yaml +patchesStrategicMerge: +- manager_tolerations_patch.yaml diff --git a/examples/provider-components/manager_tolerations_patch.yaml b/examples/provider-components/manager_tolerations_patch.yaml new file mode 100644 index 00000000..5d80c243 --- /dev/null +++ b/examples/provider-components/manager_tolerations_patch.yaml @@ -0,0 +1,56 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: capm3-controller-manager + namespace: capm3-system +spec: + template: + spec: + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/master + - key: CriticalAddonsOnly + operator: Exists +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: capi-controller-manager + namespace: capi-system +spec: + template: + spec: + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/master + - key: CriticalAddonsOnly + operator: Exists +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: capi-kubeadm-bootstrap-controller-manager + namespace: capi-kubeadm-bootstrap-system +spec: + template: + spec: + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/master + - key: CriticalAddonsOnly + operator: Exists +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: capi-kubeadm-control-plane-controller-manager + namespace: capi-kubeadm-control-plane-system +spec: + template: + spec: + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/master + - key: CriticalAddonsOnly + operator: Exists diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..7c939946 --- /dev/null +++ b/go.mod @@ -0,0 +1,81 @@ +module github.com/metal3-io/ipam + +go 1.13 + +require ( + 4d63.com/gochecknoglobals v0.0.0-20190306162314-7c3491d2b6ec // indirect + 4d63.com/gochecknoinits v0.0.0-20200108094044-eb73b47b9fc4 // indirect + cloud.google.com/go v0.56.0 // indirect + github.com/alecthomas/gocyclo v0.0.0-20150208221726-aa8f8b160214 // indirect + github.com/alexkohler/nakedret v1.0.0 // indirect + github.com/go-logr/logr v0.1.0 + github.com/go-openapi/swag v0.19.9 // indirect + github.com/golang/mock v1.4.3 + github.com/golang/protobuf v1.4.0 // indirect + github.com/google/gofuzz v1.1.0 + github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf // indirect + github.com/imdario/mergo v0.3.9 // indirect + github.com/jgautheron/goconst v0.0.0-20200227150835-cda7ea3bf591 // indirect + github.com/mdempsky/unconvert v0.0.0-20200228143138-95ecdbfc0b5f // indirect + github.com/metal3-io/baremetal-operator v0.0.0-20200424085833-a1dd8aca186d + github.com/metal3-io/cluster-api-provider-metal3 v0.3.1 + github.com/mibk/dupl v1.0.0 // indirect + github.com/onsi/ginkgo v1.12.0 + github.com/onsi/gomega v1.9.0 + github.com/opennota/check v0.0.0-20180911053232-0c771f5545ff // indirect + github.com/pkg/errors v0.9.1 + github.com/stripe/safesql v0.2.0 // indirect + github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9 // indirect + github.com/walle/lll v1.0.1 // indirect + golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5 // indirect + golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 + golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f // indirect + golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect + google.golang.org/appengine v1.6.6 // indirect + gopkg.in/yaml.v2 v2.2.8 + k8s.io/api v0.19.0-alpha.2 + k8s.io/apiextensions-apiserver v0.18.2 + k8s.io/apimachinery v0.19.0-alpha.2 + k8s.io/client-go v12.0.0+incompatible + k8s.io/cluster-bootstrap v0.18.2 // indirect + k8s.io/klog v1.0.0 + k8s.io/kube-openapi v0.0.0-20200413232311-afe0b5e9f729 // indirect + k8s.io/utils v0.0.0-20200414100711-2df71ebbae66 + mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect + mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect + mvdan.cc/unparam v0.0.0-20200314162735-0ac8026f7d06 // indirect + sigs.k8s.io/cluster-api v0.3.3 + sigs.k8s.io/controller-runtime v0.6.0 + sigs.k8s.io/kind v0.8.0 // indirect + sigs.k8s.io/yaml v1.2.0 +) + +replace ( + k8s.io/api => k8s.io/api v0.19.0-alpha.2 + k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.19.0-alpha.2 + k8s.io/apimachinery => k8s.io/apimachinery v0.19.0-alpha.2 + k8s.io/apiserver => k8s.io/apiserver v0.19.0-alpha.2 + k8s.io/cli-runtime => k8s.io/cli-runtime v0.19.0-alpha.2 + k8s.io/client-go => k8s.io/client-go v0.19.0-alpha.2 + k8s.io/cloud-provider => k8s.io/cloud-provider v0.19.0-alpha.2 + k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.19.0-alpha.2 + k8s.io/code-generator => k8s.io/code-generator v0.19.0-alpha.2 + k8s.io/component-base => k8s.io/component-base v0.19.0-alpha.2 + k8s.io/cri-api => k8s.io/cri-api v0.19.0-alpha.2 + k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.19.0-alpha.2 + k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.19.0-alpha.2 + k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.19.0-alpha.2 + k8s.io/kube-proxy => k8s.io/kube-proxy v0.19.0-alpha.2 + k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.19.0-alpha.2 + k8s.io/kubectl => k8s.io/kubectl v0.19.0-alpha.2 + k8s.io/kubelet => k8s.io/kubelet v0.19.0-alpha.2 + k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.19.0-alpha.2 + k8s.io/metrics => k8s.io/metrics v0.19.0-alpha.2 + k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.19.0-alpha.2 +) // Required by BMO + +replace github.com/docker/docker => github.com/moby/moby v0.7.3-0.20190826074503-38ab9da00309 // Required by BMO + +replace github.com/openshift/api => github.com/openshift/api v0.0.0-20190924102528-32369d4db2ad // Required by BMO until https://github.com/operator-framework/operator-lifecycle-manager/pull/1241 is resolved + +//replace github.com/googleapis/gnostic => github.com/googleapis/gnostic v0.4.0 // Issue with go-client version diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..7b6149c9 --- /dev/null +++ b/go.sum @@ -0,0 +1,1710 @@ +4d63.com/gochecknoglobals v0.0.0-20190306162314-7c3491d2b6ec h1:LArU+LLj2RRPagtrxuBE+xs+zJzOCWvX99TIoGac1gE= +4d63.com/gochecknoglobals v0.0.0-20190306162314-7c3491d2b6ec/go.mod h1:Sk40JNJmh0koZukOjJfaBNLZazbZthFfHnLHIcZNS6A= +4d63.com/gochecknoinits v0.0.0-20200108094044-eb73b47b9fc4 h1:bf5qocEKjrY58JO2GwywfLsb1199lIVs7qHkiplwHy0= +4d63.com/gochecknoinits v0.0.0-20200108094044-eb73b47b9fc4/go.mod h1:4o1i5aXtIF5tJFt3UD1knCVmWOXg7fLYdHVu6jeNcnM= +bitbucket.org/bertimus9/systemstat v0.0.0-20180207000608-0eeff89b0690/go.mod h1:Ulb78X89vxKYgdL24HMTiXYHlyHEvruOj1ZPlqeNEZM= +bou.ke/monkey v1.0.1/go.mod h1:FgHuK96Rv2Nlf+0u1OOVDpCMdsWyOFmeeketDHE7LIg= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= +cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.47.0/go.mod h1:5p3Ky/7f3N10VBkhuR5LFtddroTiMyjZV/Kj5qOQFxU= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0 h1:MZQCQQaRwOrAcuKjiHWHrgKykt4fZyuwF2dtiG3fGW8= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0 h1:3ithwDMr7/3vpAMXiH+ZQnYbuIsh+OPhUPMFC9enmn0= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.55.0 h1:eoz/lYxKSL4CNAiaUJ0ZfD1J3bfMYbU5B3rwM1C1EIU= +cloud.google.com/go v0.55.0/go.mod h1:ZHmoY+/lIMNkN2+fBmuTiqZ4inFhvQad8ft7MT8IV5Y= +cloud.google.com/go v0.56.0 h1:WRz29PgAsVEyPSDHyk+0fpEkwEFyfhHn+JbksT6gIL4= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/azure-sdk-for-go v32.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v35.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v40.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-autorest v11.1.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest v0.9.6/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/adal v0.6.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc= +github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM= +github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc= +github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/GoogleCloudPlatform/k8s-cloud-provider v0.0.0-20190822182118-27a4ced34534/go.mod h1:iroGtC8B3tQiqtds1l+mgk/BBOrxbqjH+eUfFQYRc14= +github.com/JeffAshton/win_pdh v0.0.0-20161109143554-76bb4ee9f0ab/go.mod h1:3VYc5hodBMJ5+l/7J4xAyMeuM2PNuepvHlGs8yilUCA= +github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= +github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/semver/v3 v3.0.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/sprig/v3 v3.0.0/go.mod h1:NEUY/Qq8Gdm2xgYA+NwJM6wmfdRV9xkh8h/Rld20R0U= +github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= +github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= +github.com/Microsoft/go-winio v0.4.12/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= +github.com/Microsoft/hcsshim v0.0.0-20190417211021-672e52e9209d/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/NYTimes/gziphandler v1.0.1/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/Nordix/cluster-api v0.3.0 h1:JqaGEqUUJhi0qbZMWrtsVe+UyRUH+HErHRuhiraAehA= +github.com/Nordix/cluster-api v0.3.0/go.mod h1:BqDbKtcy8z6SW7S+XxVYOZ8D7URC5g6XBh1bpZIpABc= +github.com/Nordix/cluster-api v0.3.1 h1:1UewILjb6WFjzYr9HxROe2REdOoFgOTmZa4nRaRCBSY= +github.com/Nordix/cluster-api v0.3.1/go.mod h1:OCxI4X95BA8R+NL71nRDKYm6Uu87iT9KrCi+OZVb+kE= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/Rican7/retry v0.1.0/go.mod h1:FgOROf8P5bebcC1DS0PdOQiqGUridaZvikzUmkFW6gg= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +github.com/alecthomas/gocyclo v0.0.0-20150208221726-aa8f8b160214 h1:YI/8G3uLbYyowJeOPVL6BMKe2wbL54h0FdEKmncU6lU= +github.com/alecthomas/gocyclo v0.0.0-20150208221726-aa8f8b160214/go.mod h1:Ef5UOtJdJ5rVFObdOVsrNgKV/Wf4I+daTCSk8GTrHIk= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alessio/shellescape v0.0.0-20190409004728-b115ca0f9053/go.mod h1:xW8sBma2LE3QxFSzCnH9qe6gAE2yO9GvQaWwX89HxbE= +github.com/alessio/shellescape v1.2.2 h1:8LnL+ncxhWT2TR00dfJRT25JWWrhkMZXneHVWnetDZg= +github.com/alessio/shellescape v1.2.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= +github.com/alexflint/go-arg v0.0.0-20160306200701-e71d6514f40a h1:Bc+P30eTWphhueyACA/fjiHJXRDq/kGiqO38nGxvml0= +github.com/alexflint/go-arg v0.0.0-20160306200701-e71d6514f40a/go.mod h1:PHxo6ZWOLVMZZgWSAqBynb/KhIqoGO6WKwOVX7rM9dg= +github.com/alexkohler/nakedret v1.0.0 h1:S/bzOFhZHYUJp6qPmdXdFHS5nlWGFmLmoc8QOydvotE= +github.com/alexkohler/nakedret v1.0.0/go.mod h1:tfDQbtPt67HhBK/6P0yNktIX7peCxfOp0jO9007DrLE= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/ant31/crd-validation v0.0.0-20180702145049-30f8a35d0ac2/go.mod h1:X0noFIik9YqfhGYBLEHg8LJKEwy7QIitLQuFMpKLcPk= +github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/auth0/go-jwt-middleware v0.0.0-20170425171159-5493cabe49f7/go.mod h1:LWMyo4iOLWXHGdBki7NIht1kHru/0wM179h+d3g8ATM= +github.com/aws/aws-sdk-go v1.16.26/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.28.2/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/bazelbuild/bazel-gazelle v0.0.0-20181012220611-c728ce9f663e/go.mod h1:uHBSeeATKpVazAACZBDPL/Nk/UhQDDsJWDlqYJo8/Us= +github.com/bazelbuild/buildtools v0.0.0-20180226164855-80c7f0d45d7e/go.mod h1:5JP0TXzWDHXv8qvxRC4InIazwdyDseBDbzESUMKk1yU= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bifurcation/mint v0.0.0-20180715133206-93c51c6ce115/go.mod h1:zVt7zX3K/aDCk9Tj+VM7YymsX66ERvzCJzw8rFCX2JU= +github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/brancz/gojsontoyaml v0.0.0-20190425155809-e8bd32d46b3d/go.mod h1:IyUJYN1gvWjtLF5ZuygmxbnsAyP3aJS6cHzIuZY50B0= +github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/bugsnag/bugsnag-go v1.5.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/caddyserver/caddy v1.0.3/go.mod h1:G+ouvOY32gENkJC+jhgl62TyhvqEsFaDiZ4uw0RzP1E= +github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= +github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/prettybench v0.0.0-20150116022406-03b8cfe5406c/go.mod h1:Xe6ZsFhtM8HrDku0pxJ3/Lr51rwykrzgFwpmTzleatY= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= +github.com/checkpoint-restore/go-criu v0.0.0-20190109184317-bdb7599cd87b/go.mod h1:TrMrLQfeENAPYPRsJuq3jsqdlRh3lvi6trTZJG8+tho= +github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/cfssl v0.0.0-20180726162950-56268a613adf h1:eOyFuj3h/Vj5e4voOM16NNrHsUR3jhD0duh76LHMj6Y= +github.com/cloudflare/cfssl v0.0.0-20180726162950-56268a613adf/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= +github.com/clusterhq/flocker-go v0.0.0-20160920122132-2b8b7259d313/go.mod h1:P1wt9Z3DP8O6W3rvwCt0REIlshg1InHImaLW0t3ObY0= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codegangsta/negroni v1.0.0/go.mod h1:v0y3T5G7Y1UlFfyxFn/QLRU4a2EuNau2iZY63YTKWo0= +github.com/container-storage-interface/spec v1.1.0/go.mod h1:6URME8mwIBbpVyZV93Ce5St17xBiQJQY67NDsuohiy4= +github.com/containerd/console v0.0.0-20170925154832-84eeaae905fa/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/containerd v1.0.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.2.7/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.0-beta.2.0.20190823190603-4a2f61c4f2b4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/typeurl v0.0.0-20190228175220-2a93cfde8c20/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= +github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/coredns/corefile-migration v1.0.2/go.mod h1:OFwBp/Wc9dJt5cAZzHWMNhK1r5L0p0jDwIBc6j8NC8E= +github.com/coredns/corefile-migration v1.0.7/go.mod h1:OFwBp/Wc9dJt5cAZzHWMNhK1r5L0p0jDwIBc6j8NC8E= +github.com/coreos/bbolt v1.3.1-coreos.6/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.15+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-oidc v0.0.0-20180117170138-065b426bd416/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.0.0-20180108230905-e214231b295a/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/prometheus-operator v0.34.0/go.mod h1:Li6rMllG/hYIyXfMuvUwhyC+hqwJVHdsDdP21hypT1M= +github.com/coreos/rkt v1.30.0/go.mod h1:O634mlH6U7qk87poQifK6M2rsFNt+FyUTWNMnP1hF1U= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= +github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8= +github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f/go.mod h1:8S58EK26zhXSxzv7NQFpnliaOQsmDUxvoQO3rt154Vg= +github.com/cznic/golex v0.0.0-20170803123110-4ab7c5e190e4/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc= +github.com/cznic/internal v0.0.0-20180608152220-f44710a21d00/go.mod h1:olo7eAdKwJdXxb55TKGLiJ6xt1H0/tiiRCWKVLmtjY4= +github.com/cznic/lldb v1.1.0/go.mod h1:FIZVUmYUVhPwRiPzL8nD/mpFcJ/G7SSXjjXYG4uRI3A= +github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= +github.com/cznic/ql v1.2.0/go.mod h1:FbpzhyZrqr0PVlK6ury+PoW3T0ODUV22OeWIxcaOrSE= +github.com/cznic/sortutil v0.0.0-20150617083342-4c7342852e65/go.mod h1:q2w6Bg5jeox1B+QkJ6Wp/+Vn0G/bo3f1uY7Fn3vivIQ= +github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc= +github.com/cznic/zappy v0.0.0-20160723133515-2533cb5b45cc/go.mod h1:Y1SNZ4dRUOKXshKUbwUapqNncRrho4mkjQebgEHZLj8= +github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE= +github.com/deislabs/oras v0.7.0/go.mod h1:sqMKPG3tMyIX9xwXUBRLhZ24o+uT4y6jgBD2RzUTKDM= +github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= +github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dhui/dktest v0.3.0/go.mod h1:cyzIUfGsBEbZ6BT7tnXqAShHSXCZhSNmFl70sZ7c1yc= +github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= +github.com/docker/cli v0.0.0-20190506213505-d88565df0c2d/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.7.0+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.6.1/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= +github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-metrics v0.0.0-20181218153428-b84716841b82/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= +github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libnetwork v0.0.0-20180830151422-a9cd636e3789/go.mod h1:93m0aTqz6z+g32wla4l4WxTrdtvBRmVzYRkYvasA5Z8= +github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dukov/baremetal-operator v0.0.0-20200326114222-ea14dc6e2e1d h1:1YqAhOKhBHjMMlpRWCaIRaMqoZhKWsTR18lNCxT2y3o= +github.com/dukov/baremetal-operator v0.0.0-20200326114222-ea14dc6e2e1d/go.mod h1:PYdmK4Q79XBhGn6gd1P1wUcUW//4u/koQg0Fp2FWUoE= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.6+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.11.1+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.11.2+incompatible h1:Z4Z0K2AuOw+QtgwkkJnwpT165MBr12qS8rnBwjP/Pzs= +github.com/emicklei/go-restful v2.11.2+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.12.0+incompatible h1:SIvoTSbsMEwuM3dzFirLwKc4BH6VXP5CNf+G1FfJVr4= +github.com/emicklei/go-restful v2.12.0+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/euank/go-kmsg-parser v2.0.0+incompatible/go.mod h1:MhmAMZ8V4CYH4ybgdRwPr2TU5ThnS43puaKEMpja1uw= +github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.1.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5IcNrDCXZ6OoTAWu7M= +github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.0.0 h1:dKTrUeykyQwKb/kx7Z+4ukDs6l+4L41HqG1XHnhX7WE= +github.com/evanphx/json-patch/v5 v5.0.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= +github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= +github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/structtag v1.1.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsouza/fake-gcs-server v1.7.0/go.mod h1:5XIRs4YvwNbNoz+1JF8j6KLAyDh7RHGAyAK3EP2EsNk= +github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v0.0.0-20180820084758-c7ce16629ff4/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= +github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-acme/lego v2.5.0+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M= +github.com/go-bindata/go-bindata v3.1.1+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v0.1.0 h1:M1Tv3VzNlEHg6uyACnRdtrploV2P7wZqH8BoQMtz0cg= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/zapr v0.1.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= +github.com/go-logr/zapr v0.1.1 h1:qXBXPDdNncunGs7XeEpsJt8wCjYBygluzfdLO0G5baE= +github.com/go-logr/zapr v0.1.1/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= +github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= +github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.17.2/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= +github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.17.2/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.19.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.19.2 h1:A9+F4Dc/MCNB5jibxf6rRvOvR/iFgQdyNx9eIhnGqq0= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.2 h1:o20suLFB4Ri0tuzpWtyHlh7E7HnkqTNLq6aR6WVNS1w= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3 h1:5cxNfTy0UVC3X8JL5ymxzyoUZmo8iZb+jeTWn7tUa8o= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.17.2/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= +github.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCsn0N0ZrQsk= +github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= +github.com/go-openapi/runtime v0.17.2/go.mod h1:QO936ZXeisByFmZEO1IS1Dqhtf4QV1sYYFtIq6Ld86Q= +github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= +github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.17.2/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.19.2 h1:SStNd1jRcYtfKCN7R0laGNs80WYYvn5CbBjM2sOmCrE= +github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.19.4/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.19.6 h1:rMMMj8cV38KVXK7SFc+I2MWClbEfbK705+j+dyqun5g= +github.com/go-openapi/spec v0.19.6/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +github.com/go-openapi/spec v0.19.7 h1:0xWSeMd35y5avQAThZR2PkEuqSosoS5t6gDH4L8n11M= +github.com/go-openapi/spec v0.19.7/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= +github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.17.2/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.19.2 h1:jvO6bCMBEilGwMfHhrd61zIID4oIFdwb76V17SM88dE= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.4/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.7 h1:VRuXN2EnMSsZdauzdss6JBC29YotDqG59BZ+tdlIL1s= +github.com/go-openapi/swag v0.19.7/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= +github.com/go-openapi/swag v0.19.8 h1:vfK6jLhs7OI4tAXkvkooviaE1JEPcw3mutyegLHHjmk= +github.com/go-openapi/swag v0.19.8/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= +github.com/go-openapi/swag v0.19.9 h1:1IxuqvBUU3S2Bi4YC7tlP9SJF1gVpCvqN0T2Qof4azE= +github.com/go-openapi/swag v0.19.9/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= +github.com/go-openapi/validate v0.17.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= +github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= +github.com/go-ozzo/ozzo-validation v3.5.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobuffalo/envy v1.6.5/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= +github.com/gobuffalo/flect v0.1.5/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= +github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= +github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= +github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk= +github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0= +github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/gofrs/flock v0.7.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.2.2-0.20190730201129-28a6bbf47e48/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.0 h1:G8O7TerXerS4F6sx9OV7/nRfJdnXgHZu/S/7F2SN+UE= +github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang-migrate/migrate/v4 v4.6.2/go.mod h1:JYi6reN3+Z734VZ0akNuyOJNcrg45ZL7LDBMW3WGJL0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7 h1:u4bArs140e9+AfE52mFHOXVFnOSBJBRlzTHrOPLOIhE= +github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20181024230925-c65c006176ff/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 h1:uHTyIjqVhYRhLbJ8nIiOJHkEZZ+5YoOsAbD3sk82NiE= +github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7 h1:2hRPrmiwPrp3fQX967rNJIhQPtiGXdlQWAxKbKw3VHA= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0 h1:Rd1kQnQu0Hq3qvJppYSG0HtP+f5LPPUiDswTLiEegLg= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1 h1:ocYkMQY5RrXTYgXl7ICpV0IXwlEQGwKIsery4gyXa1U= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450/go.mod h1:Bk6SMAONeMXrxql8uvOKuAZSu8aM5RUGv+1C6IJaEho= +github.com/golangplus/fmt v0.0.0-20150411045040-2a5d6d7d2995/go.mod h1:lJgMEyOkYFkPcDKwRXegd+iM6E7matEszMG5HhwytU8= +github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk= +github.com/google/btree v0.0.0-20160524151835-7d79101e329e/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/cadvisor v0.34.0/go.mod h1:1nql6U13uTHaLYB8rLS5x9IJc2qT6Xd/Tr1sTX6NE48= +github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.3.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.3.1 h1:WeAefnSUHlBb0iJKwxFDZdbfGwkd7xRNuV+IpXMJhYk= +github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU= +github.com/googleapis/gnostic v0.4.0 h1:BXDUo8p/DaxC+4FJY/SSx3gvnx9C1VdHNgaUkiEL5mk= +github.com/googleapis/gnostic v0.4.0/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU= +github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I= +github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4= +github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gophercloud/gophercloud v0.2.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gophercloud/gophercloud v0.3.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gophercloud/gophercloud v0.6.0/go.mod h1:GICNByuaEBibcjmjvI7QvYJSZEbGkcYwAR7EZK2WMqM= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf h1:vc7Dmrk4JwS0ZPS6WZvWlwDflgDTA26jItmbSj83nug= +github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gosuri/uitable v0.0.1/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= +github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/gregjones/httpcache v0.0.0-20190203031600-7a902570cb17/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20190222133341-cfaf5686ec79/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.1.0/go.mod h1:f5nM7jw/oeRSadq3xCzHAvxcr8HZnzsqU6ILg/0NiiE= +github.com/grpc-ecosystem/go-grpc-prometheus v0.0.0-20170330212424-2500245aa611/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.3.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.4/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-health-probe v0.2.1-0.20181220223928-2bf0a5b182db/go.mod h1:uBKkC2RbarFsvS5jMJHpVhTLvGlGQj9JJwkaePE3FWI= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk= +github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/heketi/heketi v9.0.0+incompatible/go.mod h1:bB9ly3RchcQqsQ9CpyaQwvva7RS5ytVoSoholZQON6o= +github.com/heketi/rest v0.0.0-20180404230133-aa6a65207413/go.mod h1:BeS3M108VzVlmAue3lv2WcGuPAX94/KN63MUURzbYSI= +github.com/heketi/tests v0.0.0-20151005000721-f3775cbcefd6/go.mod h1:xGMAM8JLi7UkZt1i4FQeQy0R2T8GLUwQhOP5M1gBhy4= +github.com/heketi/utils v0.0.0-20170317161834-435bc5bdfa64/go.mod h1:RYlF4ghFZPPmk2TC5REt5OFwvfb6lzxFWrTWB+qs28s= +github.com/helm/helm-2to3 v0.2.0/go.mod h1:jQUVAWB0bM7zNIqKPIfHFzuFSK0kHYovJrjO+hqcvRk= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= +github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= +github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= +github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg= +github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/improbable-eng/thanos v0.3.2/go.mod h1:GZewVGILKuJVPNRn7L4Zw+7X96qzFOwj63b22xYGXBE= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ= +github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jgautheron/goconst v0.0.0-20200227150835-cda7ea3bf591 h1:x/BpEhm6aL26o4TLtcU0loJ7B3+69jielrGc70V7Yb4= +github.com/jgautheron/goconst v0.0.0-20200227150835-cda7ea3bf591/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= +github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= +github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/jonboulle/clockwork v0.0.0-20141017032234-72f9bd7c4e0c/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jsonnet-bundler/jsonnet-bundler v0.1.0/go.mod h1:YKsSFc9VFhhLITkJS3X2PrRqWG9u2Jq99udTdDjQLfM= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/karrick/godirwalk v1.7.5/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34= +github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/keleustes/cluster-api v1.16.2-keleustes.20191102 h1:R3hG22joRDSf8U5OlMIqOa1fJ+NlZXF9rnzZoe1uewQ= +github.com/keleustes/cluster-api v1.16.2-keleustes.20191102/go.mod h1:v8ial/eaQrMG1Q7Oe8w+JpIKJx1YZIzmur+1yTBoRDs= +github.com/keleustes/controller-runtime v1.16.2-keleustes.20191102 h1:LiGlSdA8uCgHT/VCRUoNb2HNtp/MQnH+lX/LnPgLDps= +github.com/keleustes/controller-runtime v1.16.2-keleustes.20191102/go.mod h1:KIR9ukewTstf6A4ewvCSAg2tarz6cjSHJdjxCBU+YLg= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0 h1:reN85Pxc5larApoH1keMBiu2GWtPqXQ1nc9gx+jOU+E= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kshvakov/clickhouse v1.3.5/go.mod h1:DMzX7FxRymoNkVgizH0DWAL8Cur7wHLgx3MUnGwJqpE= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/libopenstorage/openstorage v1.0.0/go.mod h1:Sp1sIObHjat1BeXhfMqLZ14wnOzEhNx2YQedreMcUyc= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= +github.com/lpabon/godbc v0.1.1/go.mod h1:Jo9QV0cf3U6jZABgiJ2skINAXb9j8m51r07g4KI92ZA= +github.com/lucas-clemente/aes12 v0.0.0-20171027163421-cd47fb39b79f/go.mod h1:JpH9J1c9oX6otFSgdUHwUBUizmKlrMjxWnIAjff4m04= +github.com/lucas-clemente/quic-clients v0.1.0/go.mod h1:y5xVIEoObKqULIKivu+gD/LU90pL73bTdtQjPBvtCBk= +github.com/lucas-clemente/quic-go v0.10.2/go.mod h1:hvaRS9IHjFLMq76puFJeWNfmn+H70QZ/CXoxqw9bzao= +github.com/lucas-clemente/quic-go-certificates v0.0.0-20160823095156-d2f86524cced/go.mod h1:NCcRLrOTZbzhZvixZLlERbJtDtYsmMw8Jc4vS8Z0g58= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63 h1:nTT4s92Dgz2HlrB2NaMgvlfqHH39OgMhA7z3PK7PGD4= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mailru/easyjson v0.7.1 h1:mdxE1MF9o53iCb2Ghj1VfWvh7ZOwHpnVG/xwXrV90U8= +github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/maorfr/helm-plugin-utils v0.0.0-20181205064038-588190cb5e3b/go.mod h1:p3gwmRSFqbWw6plBpR0sKl3n3vpu8kX70gvCJKMvvCA= +github.com/markbates/inflect v1.0.4/go.mod h1:1fR9+pO2KHEO9ZRtto13gDwwZaAKstQzferVeWqbgNs= +github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk= +github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8= +github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a/go.mod h1:M1qoD/MqPgTZIk0EWKB38wE28ACRfVcn+cU08jyArI0= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.6/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-shellwords v1.0.5/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/maxbrunsfeld/counterfeiter/v6 v6.2.1/go.mod h1:F9YacGpnZbLQMzuPI0rR6op21YvNu/RjL705LJJpM3k= +github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= +github.com/mdempsky/maligned v0.0.0-20180708014732-6e39bd26a8c8 h1:zvpKif6gkrh82wAd2JIffdLyCL52N8r+ABwHxdIOvWM= +github.com/mdempsky/maligned v0.0.0-20180708014732-6e39bd26a8c8/go.mod h1:oGVD62YTpMEWw0JqJ2Vl48dzHywJBMlapkfsmhtokOU= +github.com/mdempsky/unconvert v0.0.0-20200228143138-95ecdbfc0b5f h1:Kc3s6QFyh9DLgInXpWKuG+8I7R7lXbnP7mcoOVIt6KY= +github.com/mdempsky/unconvert v0.0.0-20200228143138-95ecdbfc0b5f/go.mod h1:AmCV4WB3cDMZqgPk+OUQKumliiQS4ZYsBt3AXekyuAU= +github.com/mesos/mesos-go v0.0.9/go.mod h1:kPYCMQ9gsOXVAle1OsoY4I1+9kPu8GHkf88aV59fDr4= +github.com/metal3-io/baremetal-operator v0.0.0-20191004200613-f048f3bc5f05 h1:kMrNy4ECEuGH5vmd1m0kLDHVTHSvX4yw086Vb1+Scrc= +github.com/metal3-io/baremetal-operator v0.0.0-20191004200613-f048f3bc5f05/go.mod h1:o9ta8R2EEtSiQY53sXdoM50v5531G0oS+lC58Gcm+1Y= +github.com/metal3-io/baremetal-operator v0.0.0-20200116072141-3d7a06eb80b9 h1:mVvpTmqOAqwT37jkQaqCq5umiiywZnLA51lS7z/eNqU= +github.com/metal3-io/baremetal-operator v0.0.0-20200116072141-3d7a06eb80b9/go.mod h1:o9ta8R2EEtSiQY53sXdoM50v5531G0oS+lC58Gcm+1Y= +github.com/metal3-io/baremetal-operator v0.0.0-20200207092945-ce276d44ddec h1:cbGYTPHWO5ErkHIPCV9clnHixp43nzeOj8KEU9E2GXU= +github.com/metal3-io/baremetal-operator v0.0.0-20200207092945-ce276d44ddec/go.mod h1:o9ta8R2EEtSiQY53sXdoM50v5531G0oS+lC58Gcm+1Y= +github.com/metal3-io/baremetal-operator v0.0.0-20200220133300-43fab21f7d0a h1:7rn11nuFUx/yKyf76K8Nww2UkrigvQzHrHy4fvrXV38= +github.com/metal3-io/baremetal-operator v0.0.0-20200220133300-43fab21f7d0a/go.mod h1:o9ta8R2EEtSiQY53sXdoM50v5531G0oS+lC58Gcm+1Y= +github.com/metal3-io/baremetal-operator v0.0.0-20200225121200-8161ce57c5af h1:9G33rYnHv2n/ICymnRsVev+LSL7J5BWvM6Al+hiXxX0= +github.com/metal3-io/baremetal-operator v0.0.0-20200225121200-8161ce57c5af/go.mod h1:o9ta8R2EEtSiQY53sXdoM50v5531G0oS+lC58Gcm+1Y= +github.com/metal3-io/baremetal-operator v0.0.0-20200303095345-4b4f065f311b h1:ipSqipnh+i7Iqrgmay5cDfOZ/TCiqMEgjNNMODK9Fbg= +github.com/metal3-io/baremetal-operator v0.0.0-20200303095345-4b4f065f311b/go.mod h1:aJeLHszKLl8uCD7ZZz6KfLKgGXluo4hUBwL7X8YLs7E= +github.com/metal3-io/baremetal-operator v0.0.0-20200318114549-c3da1db56f43 h1:rKycVcdWOQ5wkzartz+ThvmuT4VnusRdytFFR+T5jyI= +github.com/metal3-io/baremetal-operator v0.0.0-20200318114549-c3da1db56f43/go.mod h1:aJeLHszKLl8uCD7ZZz6KfLKgGXluo4hUBwL7X8YLs7E= +github.com/metal3-io/baremetal-operator v0.0.0-20200323093448-f696b0786c9e h1:6rmk+fF+QHex1WzC66eB16Edwc1qVSXgdHS2TJ0wf9U= +github.com/metal3-io/baremetal-operator v0.0.0-20200323093448-f696b0786c9e/go.mod h1:aJeLHszKLl8uCD7ZZz6KfLKgGXluo4hUBwL7X8YLs7E= +github.com/metal3-io/baremetal-operator v0.0.0-20200325151250-5ba84fbafea9 h1:D6zWbSP1AdqCP5MJIAm6FkIQD9tiauzhI+HQbbYeCAU= +github.com/metal3-io/baremetal-operator v0.0.0-20200325151250-5ba84fbafea9/go.mod h1:aJeLHszKLl8uCD7ZZz6KfLKgGXluo4hUBwL7X8YLs7E= +github.com/metal3-io/baremetal-operator v0.0.0-20200328061249-10eb5aa3e614 h1:JYH8gY6URbR+W7TNqgw3ME7H3U/iTi3Na/MSW1xx0kc= +github.com/metal3-io/baremetal-operator v0.0.0-20200328061249-10eb5aa3e614/go.mod h1:aJeLHszKLl8uCD7ZZz6KfLKgGXluo4hUBwL7X8YLs7E= +github.com/metal3-io/baremetal-operator v0.0.0-20200424085833-a1dd8aca186d h1:+z1MMMC1tZFlDZdB124xkOcsxT1++u4s0psHuZFAX0o= +github.com/metal3-io/baremetal-operator v0.0.0-20200424085833-a1dd8aca186d/go.mod h1:hb+evBgPvwwPomUzeN5okBOf45g1ZWbQ7rfrVp7sMTY= +github.com/metal3-io/cluster-api-provider-metal3 v0.3.1 h1:CRG6QHbCITRTCJAbZuRlEdc7NNzgLgobW9RcE7PJNOE= +github.com/metal3-io/cluster-api-provider-metal3 v0.3.1/go.mod h1:Zh08qwz1xhpEhuSXptctZMi6gJDz2nWTYqtB2Dxo2xo= +github.com/mholt/certmagic v0.6.2-0.20190624175158-6a42ef9fe8c2/go.mod h1:g4cOPxcjV0oFq3qwpjSA30LReKD8AoIfwAY9VvG35NY= +github.com/mibk/dupl v1.0.0 h1:aZc3jqrF9n0tUHwHt/+jsRxA8cRgA0Gdl56M7W7PoqE= +github.com/mibk/dupl v1.0.0/go.mod h1:pCr4pNxxIbFGvtyCOi0c7LVjmV6duhKWV+ex5vh38ME= +github.com/miekg/dns v0.0.0-20181005163659-0d29b283ac0f/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.3/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.4/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mindprince/gonvml v0.0.0-20171110221305-fee913ce8fb2/go.mod h1:2eu9pRWp8mo84xCg6KswZ+USQHjwgRhNp06sozOdsTY= +github.com/mistifyio/go-zfs v2.1.1+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/hashstructure v0.0.0-20170609045927-2bca23e0e452/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ= +github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/moby v0.7.3-0.20190826074503-38ab9da00309/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= +github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mohae/deepcopy v0.0.0-20170603005431-491d3605edfb/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mozilla/tls-observatory v0.0.0-20190404164649-a3c1b6cfecfd/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk= +github.com/mozilla/tls-observatory v0.0.0-20200220173314-aae45faa4006/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk= +github.com/mozilla/tls-observatory v0.0.0-20200317151703-4fa42e1c2dee/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk= +github.com/mrunalp/fileutils v0.0.0-20160930181131-4ee1cc9a8058/go.mod h1:x8F1gnqOkIEiO4rqoeEEEqQbo7HjGMTvyoq3gej4iT0= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20190414153302-2ae31c8b6b30/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mvdan/xurls v1.1.0/go.mod h1:tQlNn3BED8bE/15hnSL2HLkDeLWpNPAwtw7wkEq44oU= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA= +github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= +github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= +github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= +github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1ORQBMvzfzBgpsctsbQikCVpvC+tX285E= +github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.4.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU= +github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.3.0/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.8.1 h1:C5Dqfs/LeauYDX0jJXIe2SWmwCbGzx9yF8C8xy3Lh34= +github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg= +github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= +github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc2.0.20190611121236-6cc515888830/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runtime-spec v1.0.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.2.2/go.mod h1:+BLncwf63G4dgOzykXAxcmnFlUaOlkDdmw/CqsW6pjs= +github.com/opennota/check v0.0.0-20180911053232-0c771f5545ff h1:lRHufowVGvUvxGsPveAZOpSa/9T5Gpxg6d7UbHCA9MQ= +github.com/opennota/check v0.0.0-20180911053232-0c771f5545ff/go.mod h1:tydB+MZxWpY8M/NRu7jQhND/mXuLAPsKcSV6JkzofsA= +github.com/openshift/api v0.0.0-20190924102528-32369d4db2ad/go.mod h1:dh9o4Fs58gpFXGSYfnVxGR9PnV53I8TW84pQaJDdGiY= +github.com/openshift/client-go v0.0.0-20190923180330-3b6373338c9b/go.mod h1:6rzn+JTr7+WYS2E1TExP4gByoABxMznR6y2SnUIkmxk= +github.com/openshift/origin v0.0.0-20160503220234-8f127d736703/go.mod h1:0Rox5r9C8aQn6j1oAOQ0c1uC86mYbUFObzjBRvUKHII= +github.com/openshift/prom-label-proxy v0.1.1-0.20191016113035-b8153a7f39f1/go.mod h1:p5MuxzsYP1JPsNGwtjtcgRHHlGziCJJfztff91nNixw= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/operator-framework/api v0.0.0-20200120235816-80fd2f1a09c9/go.mod h1:S5IdlJvmKkF84K2tBvsrqJbI2FVy03P88R75snpRxJo= +github.com/operator-framework/operator-lifecycle-manager v0.0.0-20191115003340-16619cd27fa5/go.mod h1:zL34MNy92LPutBH5gQK+gGhtgTUlZZX03I2G12vWHF4= +github.com/operator-framework/operator-registry v1.5.1/go.mod h1:agrQlkWOo1q8U1SAaLSS2WQ+Z9vswNT2M2HFib9iuLY= +github.com/operator-framework/operator-registry v1.5.3/go.mod h1:agrQlkWOo1q8U1SAaLSS2WQ+Z9vswNT2M2HFib9iuLY= +github.com/operator-framework/operator-registry v1.5.7-0.20200121213444-d8e2ec52c19a/go.mod h1:ekexcV4O8YMxdQuPb+Xco7MHfVmRIq7Jvj5e6NU7dHI= +github.com/operator-framework/operator-sdk v0.15.1/go.mod h1:RkC5LpluVONa08ORFIIVCYrEr855xG1/NltRL2jQ8qo= +github.com/otiai10/copy v1.0.1/go.mod h1:8bMCJrAqOtN/d9oyh5HR7HhLQMvcGMpGdwRDYsfOCHc= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v0.0.0-20190513014714-f5a3d24e5776/go.mod h1:3HNVkVOU7vZeFXocWuvtcS0XSFLcf2XUSDHkq9t1jU4= +github.com/otiai10/mint v1.2.3/go.mod h1:YnfyPNhBvnY8bW4SGQHCs/aAFhkgySlMZbrF5U0bOVw= +github.com/otiai10/mint v1.2.4/go.mod h1:d+b7n/0R3tdyUYYylALXpWQ/kTN+QobSq/4SRGBkR3M= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.0.1/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= +github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/phayes/freeport v0.0.0-20171002181615-b8543db493a5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.0 h1:J8lpUdobwIeCI7OiSxHqEwJUKvJwicL5+3v1oe2Yb4k= +github.com/pkg/errors v0.9.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/pquerna/ffjson v0.0.0-20180717144149-af8b230fcd20/go.mod h1:YARuvh7BUWHNhzDq2OM5tzR2RiCcN2D7sapiKyCel/M= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740= +github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.2.1/go.mod h1:XMU6Z2MjaRKVu/dC1qupJI9SiNkDYzz3xecMgSW/F+U= +github.com/prometheus/client_golang v1.4.1 h1:FFSuS004yOQEtDdTq+TAOLP5xUq63KqAFYyOi8zA+Y8= +github.com/prometheus/client_golang v1.4.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.5.0 h1:Ctq0iGpCmr3jeP77kbF2UxgvRwzWWz+4Bh9/vJTyg1A= +github.com/prometheus/client_golang v1.5.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.5.1 h1:bdHYieyGlH+6OLEk2YQha8THib30KP0/yD0YH9m6xcA= +github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 h1:PnBWHBf+6L0jOqq0gIVUe6Yk0/QMZ640k6NvkxcBf+8= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190129233650-316cf8ccfec5/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3 h1:CTwfnzjQ+8dS6MhHHu4YswVAD99sL2wjPqP+VkURmKE= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8= +github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.0.9 h1:DksSrntiTPE63NQuxGcFa1OS/odKfwJu3PJHrhKAy7Q= +github.com/prometheus/procfs v0.0.9/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.0.10 h1:QJQN3jYQhkamO4mhfUWqdDH2asK7ONOI9MTWjyAxNKM= +github.com/prometheus/procfs v0.0.10/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.0.11 h1:DhHlBtkHWPYi8O2y31JkK0TF+DGM+51OopZjH/Ia5qI= +github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/prometheus v2.3.2+incompatible/go.mod h1:oAIUtOny2rjMX0OWN5vPR5/q/twIROJvdqnQKDdil/s= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/prometheus/tsdb v0.8.0/go.mod h1:fSI0j+IUQrDd7+ZtR9WKIGtoYAYAJUKcKhYLG25tN4g= +github.com/quobyte/api v0.1.2/go.mod h1:jL7lIHrmqQ7yh05OJ+eEEdHr0u/kmT1Ff9iHd+4H6VI= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= +github.com/robfig/cron v0.0.0-20170526150127-736158dc09e1/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= +github.com/robfig/cron v1.1.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.5.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rubenv/sql-migrate v0.0.0-20191025130928-9355dd04f4b3/go.mod h1:WS0rl9eEliYI8DPnr3TOwz4439pay+qNgzJoVya/DmY= +github.com/rubiojr/go-vhd v0.0.0-20160810183302-0bfd3b39853c/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto= +github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= +github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= +github.com/securego/gosec v0.0.0-20200203094520-d13bb6d2420c h1:pThusIwnQVcKbuZSds3HgB/ODEqxMqZf/SgVp89JXY0= +github.com/securego/gosec v0.0.0-20200203094520-d13bb6d2420c/go.mod h1:gp0gaHj0WlmPh9BdsTmo1aq6C27yIPWdxCKGFGdVKBE= +github.com/securego/gosec v0.0.0-20200302134848-c998389da2ac/go.mod h1:NurAFZsWJAEZjogSwdVPlHkOZB3DOAU7gsPP8VFZCHc= +github.com/securego/gosec v0.0.0-20200401082031-e946c8c39989/go.mod h1:i9l/TNj+yDFh9SZXUTvspXTjbFXgZGP/UvhU1S65A4A= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.3/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.0-20180319062004-c439c4fa0937/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= +github.com/storageos/go-api v0.0.0-20180912212459-343b3eff91fc/go.mod h1:ZrLn+e0ZuF3Y65PNF6dIwbJPZqfmtCXxFm9ckv0agOY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stripe/safesql v0.2.0 h1:xiefmCDd8c35PVSGrL2FhBiaKxviXnGziBDOpOejeBE= +github.com/stripe/safesql v0.2.0/go.mod h1:q7b2n0JmzM1mVGfcYpanfVb2j23cXZeWFxcILPn3JV4= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/syndtr/gocapability v0.0.0-20160928074757-e7cb7fa329f4/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/thecodeteam/goscaleio v0.1.0/go.mod h1:68sdkZAsK8bvEwBlbQnlLS+xU+hvLYM/iQ8KXej1AwM= +github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9 h1:vY5WqiEon0ZSTGM3ayVVi+twaHKHDFUVloaQ/wug9/c= +github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9/go.mod h1:q+QjxYvZ+fpjMXqs+XEriussHjSYqeXVnAdSV1tkMYk= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= +github.com/vishvananda/netlink v0.0.0-20171020171820-b2de5d10e38e/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= +github.com/vishvananda/netns v0.0.0-20171111001504-be1fbeda1936/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= +github.com/vmware/govmomi v0.20.1/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= +github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= +github.com/walle/lll v1.0.1 h1:lbK8008fOXbQNYt8daBGUrjvElvlwlE7D7N/9dLP5IQ= +github.com/walle/lll v1.0.1/go.mod h1:lYxcXzoPhiAHR9eaq+Yv7RYg1nIipLloBCIfPUzfaWQ= +github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= +github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xenolf/lego v0.0.0-20160613233155-a9d8cec0e656/go.mod h1:fwiGnfsIjG7OHPfOvgK7Y/Qo6+2Ox0iozjNTkZICKbY= +github.com/xenolf/lego v0.3.2-0.20160613233155-a9d8cec0e656/go.mod h1:fwiGnfsIjG7OHPfOvgK7Y/Qo6+2Ox0iozjNTkZICKbY= +github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yvasiyarov/go-metrics v0.0.0-20150112132944-c25f46c4b940/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= +github.com/yvasiyarov/gorelic v0.0.6/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= +github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.etcd.io/etcd v0.5.0-alpha.5.0.20200401174654-e694b7bb0875/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.0/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v0.0.0-20181018215023-8dc6146f7569/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v0.0.0-20180122172545-ddea229ff1df/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.4.0 h1:f3WCSC2KzAcBXGATIxAB1E2XuCpNU255wNKZ505qi3E= +go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v0.0.0-20180814183419-67bc79d13d15/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.12.0 h1:dySoUQPFBGj6xwjmBzageVL8jGi8uxc6bEmJQjA06bw= +go.uber.org/zap v1.12.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8 h1:1wopBVtVdWnn03fZelqdXTqk7U7zPQCb+T4rbU9ZEoU= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191028145041-f83a4685e152/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20191108234033-bd318be0434a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 h1:Sy5bstxEqwwbYs6n0/pBuxKENqOeZUgD45Gp3Q3pqLg= +golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200219234226-1ad67e1f0ef4 h1:4icQlpeqbz3WxfgP6Eq3szTj95KTrlH/CwzBzoxuFd0= +golang.org/x/crypto v0.0.0-20200219234226-1ad67e1f0ef4/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6 h1:TjszyFsQsyZNHwdVdZ5m7bjmreu0znc2kRYsEml9/Ww= +golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM= +golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5 h1:Q7tZBpemrlsc2I7IyODzhtallWRSm4Q0d09pL6XbQtU= +golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180112015858-5ccada7d0a7b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc h1:gkKoSkUmnU6bpS/VhkuO27bzQeSA51uaEfbOW5dNb68= +golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191109021931-daa7c04131f5/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200219183655-46282727080f h1:dB42wwhNuwPvh8f+5zZWNcU+F2Xs/B9wXXwvUCOH7r8= +golang.org/x/net v0.0.0-20200219183655-46282727080f/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226051749-491c5fce7268 h1:fnuNgko6vrkrxuKfTMd+0eOz50ziv+Wi+t38KUT3j+E= +golang.org/x/net v0.0.0-20200226051749-491c5fce7268/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200319234117-63522dbf7eec h1:w0SItUiQ4sBiXBAwWNkyu8Fu2Qpn/dtDIcoPkPDqjRw= +golang.org/x/net v0.0.0-20200319234117-63522dbf7eec/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U= +golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180117170059-2c42eef0765b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181004145325-8469e314837c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190102155601-82a175fd1598/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190310054646-10058d7d4faa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190426135247-a129542de9ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 h1:LepdCS8Gf/MVejFIt8lsiexZATdoGVyp5bcyS+rYoUI= +golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C17zUDepwGQBb/n+JVg= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9YmqEm0diQn9QmZw/0mU= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191028164358-195ce5e7f934/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e h1:N7DeIrjYszNmSW409R3frPPwglRwMkXSBzwVbkOjLLA= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1 h1:gZpLHxUX5BdYLA08Lj4YCJNN/jk7KtquiArPoeX0WvA= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 h1:sfkvUWPNGwSV+8/fNqctR5lS2AqCSqYwXdrjCxp/dXo= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c h1:jceGD5YNJGgGMkJz79agzOln1K9TaZUjv5ird16qniQ= +golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200317113312-5766fd39f98d h1:62ap6LNOjDU6uGmKXHJbSfciMoV+FeI1sRXx/pLDL44= +golang.org/x/sys v0.0.0-20200317113312-5766fd39f98d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200327173247-9dae0f8f5775 h1:TC0v2RSO1u2kn1ZugjrFXkRZAEaqMN/RW+OTZkBzmLE= +golang.org/x/sys v0.0.0-20200327173247-9dae0f8f5775/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f h1:gWF768j/LaZugp8dyS4UwsslYCYz9XgFxvlgsn0n9H8= +golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20171227012246-e19ae1496984/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20170824195420-5d2fd3ccab98/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190425222832-ad9eeb80039a/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac h1:MQEvx39qSf8vyrx3XRaOe+j1UDIzKwkYOVObRgGPVqI= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191010171213-8abd42400456/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191018212557-ed542cd5b28a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200203023011-6f24f261dadb h1:Mjk7HEiAvEl5eS8doSYHgS8vXw90VXegoY/vbw8uRGE= +golang.org/x/tools v0.0.0-20200203023011-6f24f261dadb/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200225230052-807dcd883420/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200226224502-204d844ad48d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200228224639-71482053b885/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200302225559-9b52d559c609/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb h1:iKlO7ROJc6SttHKlxzwGytRtBUqX4VARrNTgP2YLX5M= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200313205530-4303120df7d8 h1:gkI/wGGwpcG5W4hLCzZNGxA4wzWBGGDStRI1MrjDl2Q= +golang.org/x/tools v0.0.0-20200313205530-4303120df7d8/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200317043434-63da46f3035e h1:8ogAbHWoJTPepnVbNRqXLOpzMkl0rtRsM7crbflc4XM= +golang.org/x/tools v0.0.0-20200317043434-63da46f3035e/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4 h1:kDtqNkeBrZb8B+atrj50B5XLHpzXXqcCdZPP/ApQ5NY= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200331202046-9d5940d49312/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200417140056-c07e33ef3290 h1:NXNmtp0ToD36cui5IqWy95LC4Y6vT/4y3RnPxlQPinU= +golang.org/x/tools v0.0.0-20200417140056-c07e33ef3290/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.0.1 h1:xyiBuvkD2g5n7cYzx6u2sxQvsAy4QJsZFCzGVdzOXZ0= +gomodules.xyz/jsonpatch/v2 v2.0.1/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3mwe7XcUU= +gomodules.xyz/jsonpatch/v2 v2.1.0 h1:Phva6wqu+xR//Njw6iorylFFgn/z547tw5Ne3HZPQ+k= +gomodules.xyz/jsonpatch/v2 v2.1.0/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3mwe7XcUU= +gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= +gonum.org/v1/gonum v0.0.0-20190710053202-4340aa3071a0/go.mod h1:03dgh78c4UvU1WksguQ/lvJQXbezKQGJSrwwRq5MraQ= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/api v0.3.2/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.6.1-0.20190607001116-5213b8090861/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20170731182057-09f6ed296fc6/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190128161407-8ac453e89fca/go.mod h1:L3J43x8/uS+qIUoksaLKe6OS3nUKxOKuIFz1sl2/jx4= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191009194640-548a555dbc03/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191028173616-919d9bdd9fe6/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200317114155-1f3552e48f24/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/grpc v1.13.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.0/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw= +gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/mcuadros/go-syslog.v2 v2.2.1/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U= +gopkg.in/natefinch/lumberjack.v2 v2.0.0-20150622162204-20b71e5b60d7/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v1 v1.1.2/go.mod h1:QpYS+a4WhS+DTlyQIi6Ka7MS3SuR9a055rgXNEe6EiA= +gopkg.in/square/go-jose.v2 v2.0.0-20180411045311-89060dee6a84/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.1/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.0.0/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.1.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71 h1:Xe2gvTZUJpsvOWUnvmL/tmhVBZUmHSvLbMjRj6NUUKo= +gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +helm.sh/helm/v3 v3.0.0/go.mod h1:sI7B9yfvMgxtTPMWdk1jSKJ2aa59UyP9qhPydqW6mgo= +helm.sh/helm/v3 v3.0.1/go.mod h1:sI7B9yfvMgxtTPMWdk1jSKJ2aa59UyP9qhPydqW6mgo= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.2/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.0.0-20190918155943-95b840bb6a1f h1:8FRUST8oUkEI45WYKyD8ed7Ad0Kg5v11zHyPkEVb2xo= +k8s.io/api v0.0.0-20190918155943-95b840bb6a1f/go.mod h1:uWuOHnjmNrtQomJrvEBg0c0HRNyQ+8KTEERVsK0PW48= +k8s.io/api v0.0.0-20190918195907-bd6ac527cfd2 h1:bkwe5LsuANqyOwsBng5Qc4S91D2Tv0JHctAztt3YTQs= +k8s.io/api v0.0.0-20190918195907-bd6ac527cfd2/go.mod h1:AOxZTnaXR/xiarlQL0JUfwQPxjmKDvVYoRp58cA7lUo= +k8s.io/api v0.0.0-20191003000013-35e20aa79eb8 h1:cJZ/+fVFIFQDkUewZeim5OhZ8X+h48lMtMsZgQMdxOo= +k8s.io/api v0.0.0-20191003000013-35e20aa79eb8/go.mod h1:uWuOHnjmNrtQomJrvEBg0c0HRNyQ+8KTEERVsK0PW48= +k8s.io/api v0.17.2/go.mod h1:BS9fjjLc4CMuqfSO8vgbHPKMt5+SF0ET6u/RVDihTo4= +k8s.io/api v0.17.3 h1:XAm3PZp3wnEdzekNkcmj/9Y1zdmQYJ1I4GKSBBZ8aG0= +k8s.io/api v0.17.3/go.mod h1:YZ0OTkuw7ipbe305fMpIdf3GLXZKRigjtZaV5gzC2J0= +k8s.io/api v0.18.0 h1:lwYk8Vt7rsVTwjRU6pzEsa9YNhThbmbocQlKvNBB4EQ= +k8s.io/api v0.18.0/go.mod h1:q2HRQkfDzHMBZL9l/y9rH63PkQl4vae0xRT+8prbrK8= +k8s.io/api v0.19.0-alpha.2 h1:GVZeds8bgQOSdQ/LYcjL7+NstBByZ5L3U/Ks6+E+QRI= +k8s.io/api v0.19.0-alpha.2/go.mod h1:ujOZQ0qV79Ae02qAditrRXodlKq4GMe7khc3uS2hhEw= +k8s.io/apiextensions-apiserver v0.0.0-20190918161926-8f644eb6e783 h1:V6ndwCPoao1yZ52agqOKaUAl7DYWVGiXjV7ePA2i610= +k8s.io/apiextensions-apiserver v0.0.0-20190918161926-8f644eb6e783/go.mod h1:xvae1SZB3E17UpV59AWc271W/Ph25N+bjPyR63X6tPY= +k8s.io/apiextensions-apiserver v0.0.0-20190918201827-3de75813f604 h1:Kl/sh+wWzYK2hWFZtwvuFECup1SbE2kXfMnhGZsoO5M= +k8s.io/apiextensions-apiserver v0.0.0-20190918201827-3de75813f604/go.mod h1:7H8sjDlWQu89yWB3FhZfsLyRCRLuoXoCoY5qtwW1q6I= +k8s.io/apiextensions-apiserver v0.0.0-20191003002041-49e3d608220c h1:LaD1tVKDPnbOvH5ENJ+0AWo/i3yc5KRXsaEy/4xrF3Y= +k8s.io/apiextensions-apiserver v0.0.0-20191003002041-49e3d608220c/go.mod h1:f/4BPcq0d868y5LpOo3J4pYazaHSX+XWiGd6Vlg1qHI= +k8s.io/apiextensions-apiserver v0.17.2/go.mod h1:4KdMpjkEjjDI2pPfBA15OscyNldHWdBCfsWMDWAmSTs= +k8s.io/apiextensions-apiserver v0.17.3 h1:WDZWkPcbgvchEdDd7ysL21GGPx3UKZQLDZXEkevT6n4= +k8s.io/apiextensions-apiserver v0.17.3/go.mod h1:CJbCyMfkKftAd/X/V6OTHYhVn7zXnDdnkUjS1h0GTeY= +k8s.io/apiextensions-apiserver v0.18.0 h1:HN4/P8vpGZFvB5SOMuPPH2Wt9Y/ryX+KRvIyAkchu1Q= +k8s.io/apiextensions-apiserver v0.18.0/go.mod h1:18Cwn1Xws4xnWQNC00FLq1E350b9lUF+aOdIWDOZxgo= +k8s.io/apiextensions-apiserver v0.19.0-alpha.2 h1:lQjE543mSh4jeBxrvnwz37DCzGHW2UMefX8eCzk8uAU= +k8s.io/apiextensions-apiserver v0.19.0-alpha.2/go.mod h1:o9Z5LTfSo8QcDDR7GiXAYGi6zO61uZeAHSTQ9QI9f+Q= +k8s.io/apimachinery v0.0.0-20190817020851-f2f3a405f61d/go.mod h1:3jediapYqJ2w1BFw7lAZPCx7scubsTfosqHkhXCWJKw= +k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655 h1:CS1tBQz3HOXiseWZu6ZicKX361CZLT97UFnnPx0aqBw= +k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655/go.mod h1:nL6pwRT8NgfF8TT68DBI8uEePRt89cSvoXUVqbkWHq4= +k8s.io/apimachinery v0.17.1 h1:zUjS3szTxoUjTDYNvdFkYt2uMEXLcthcbp+7uZvWhYM= +k8s.io/apimachinery v0.17.1/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= +k8s.io/apimachinery v0.17.2/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= +k8s.io/apimachinery v0.17.3 h1:f+uZV6rm4/tHE7xXgLyToprg6xWairaClGVkm2t8omg= +k8s.io/apimachinery v0.17.3/go.mod h1:gxLnyZcGNdZTCLnq3fgzyg2A5BVCHTNDFrw8AmuJ+0g= +k8s.io/apimachinery v0.18.0 h1:fuPfYpk3cs1Okp/515pAf0dNhL66+8zk8RLbSX+EgAE= +k8s.io/apimachinery v0.18.0/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA= +k8s.io/apimachinery v0.19.0-alpha.2 h1:N155+ZeSeRnCFyzjYRv3vg9GWJIUm5ElZba66f7qicY= +k8s.io/apimachinery v0.19.0-alpha.2/go.mod h1:imoz42hIYwpLTRWXU8pdJ9IE8DbxUsnU9lyVN8Y1SNo= +k8s.io/apiserver v0.0.0-20190918160949-bfa5e2e684ad/go.mod h1:XPCXEwhjaFN29a8NldXA901ElnKeKLrLtREO9ZhFyhg= +k8s.io/apiserver v0.0.0-20190918200908-1e17798da8c1/go.mod h1:4FuDU+iKPjdsdQSN3GsEKZLB/feQsj1y9dhhBDVV2Ns= +k8s.io/apiserver v0.0.0-20191003001037-3c8b233e046c h1:iFiXmUwrN0MzZ5/QaiphMCr8qHaCfg4yOJZexNeDFq8= +k8s.io/apiserver v0.0.0-20191003001037-3c8b233e046c/go.mod h1:6Y84oLCxuMhnonndiI6RbzaKnBmC88PAsQSg2nmldAs= +k8s.io/apiserver v0.17.2/go.mod h1:lBmw/TtQdtxvrTk0e2cgtOxHizXI+d0mmGQURIHQZlo= +k8s.io/apiserver v0.17.3 h1:faZbSuFtJ4dx09vctKZGHms/7bp3qFtbqb10Swswqfs= +k8s.io/apiserver v0.17.3/go.mod h1:iJtsPpu1ZpEnHaNawpSV0nYTGBhhX2dUlnn7/QS7QiY= +k8s.io/apiserver v0.18.0 h1:ELAWpGWC6XdbRLi5lwAbEbvksD7hkXxPdxaJsdpist4= +k8s.io/apiserver v0.18.0/go.mod h1:3S2O6FeBBd6XTo0njUrLxiqk8GNy6wWOftjhJcXYnjw= +k8s.io/apiserver v0.19.0-alpha.2/go.mod h1:3sm/OyOD8RpEaJ4B2MzqeGXsfVZP+T/7y+Hr0+0qfao= +k8s.io/autoscaler v0.0.0-20190607113959-1b4f1855cb8e/go.mod h1:QEXezc9uKPT91dwqhSJq3GNI3B1HxFRQHiku9kmrsSA= +k8s.io/cli-runtime v0.17.3/go.mod h1:X7idckYphH4SZflgNpOOViSxetiMj6xI0viMAjM81TA= +k8s.io/cli-runtime v0.18.0/go.mod h1:1eXfmBsIJosjn9LjEBUd2WVPoPAY9XGTqTFcPMIBsUQ= +k8s.io/cli-runtime v0.19.0-alpha.2/go.mod h1:k2LnE5XW0j4nuDrfdkiXNOxLyNRAtN8Zd5udvGL/FyI= +k8s.io/client-go v0.0.0-20190918160344-1fbdaa4c8d90/go.mod h1:J69/JveO6XESwVgG53q3Uz5OSfgsv4uxpScmmyYOOlk= +k8s.io/client-go v0.0.0-20190918200256-06eb1244587a/go.mod h1:3YAcTbI2ArBRmhHns5vlHRX8YQqvkVYpz+U/N5i1mVU= +k8s.io/client-go v0.0.0-20191003000419-f68efa97b39e h1:S7/BObCpTSVf9vlBdEs5hKNzLCq5FMha6bbmRoDGROU= +k8s.io/client-go v0.0.0-20191003000419-f68efa97b39e/go.mod h1:UBFA5lo8nEOepaxS9koNccX/38rYMI3pa1EA1gaFZNg= +k8s.io/client-go v0.17.2/go.mod h1:QAzRgsa0C2xl4/eVpeVAZMvikCn8Nm81yqVx3Kk9XYI= +k8s.io/client-go v0.17.3 h1:deUna1Ksx05XeESH6XGCyONNFfiQmDdqeqUvicvP6nU= +k8s.io/client-go v0.17.3/go.mod h1:cLXlTMtWHkuK4tD360KpWz2gG2KtdWEr/OT02i3emRQ= +k8s.io/client-go v0.18.0 h1:yqKw4cTUQraZK3fcVCMeSa+lqKwcjZ5wtcOIPnxQno4= +k8s.io/client-go v0.18.0/go.mod h1:uQSYDYs4WhVZ9i6AIoEZuwUggLVEF64HOD37boKAtF8= +k8s.io/client-go v0.19.0-alpha.2 h1:YqJuHm/xOYP2VIOWPnQO+ix+Ag5KditpdHmIreWYyTY= +k8s.io/client-go v0.19.0-alpha.2/go.mod h1:HSXneQoJ3BRnmTonpCTejgoMMcAImpvBZQZnm+DJhhg= +k8s.io/client-go v11.0.0+incompatible h1:LBbX2+lOwY9flffWlJM7f1Ct8V2SRNiMRDFeiwnJo9o= +k8s.io/client-go v11.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= +k8s.io/client-go v11.0.1-0.20190409021438-1a26190bd76a+incompatible h1:U5Bt+dab9K8qaUmXINrkXO135kA11/i5Kg1RUydgaMQ= +k8s.io/client-go v11.0.1-0.20190409021438-1a26190bd76a+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= +k8s.io/cloud-provider v0.17.3/go.mod h1:JBkKSQpbcjcYGDqH5PbifFrcgQ/7WOXRswnfLVbXpI8= +k8s.io/cloud-provider v0.18.0/go.mod h1:ZBq1FhoJ+XoQ8JYBYoyx81LS3JV0RAW/UmHf/6w9E6k= +k8s.io/cloud-provider v0.19.0-alpha.2/go.mod h1:gmNVC+ICDmO6iRe5kcHmGuBxycx/qgTJaD9F2XpdRTY= +k8s.io/cluster-bootstrap v0.0.0-20190516232516-d7d78ab2cfe7/go.mod h1:iBSm2nwo3OaiuW8VDvc3ySDXK5SKfUrxwPvBloKG7zg= +k8s.io/cluster-bootstrap v0.0.0-20191003003255-c493acd9e2ff h1:WU8lXzy1nNP9YSUzVc7VUwkq0mzP2i6O/k/gZEg8oT8= +k8s.io/cluster-bootstrap v0.0.0-20191003003255-c493acd9e2ff/go.mod h1:luv3TN5FNNeQfInnX9XeWGVjfO1cJWorNfnE1ExkfQo= +k8s.io/cluster-bootstrap v0.17.3 h1:J0fKY0kTtD9ZFaLHAoWXhjXlb4m1g9BsF2YNgMRJsxU= +k8s.io/cluster-bootstrap v0.17.3/go.mod h1:ujIYnCKnxY/MecpgPx9WgiYCVCFvici6tVIfI2FiI1g= +k8s.io/cluster-bootstrap v0.18.0 h1:fD1tYV3L0yK5fJvPIlwA2Z/zehrTkkdSqtZBOn+LUr4= +k8s.io/cluster-bootstrap v0.18.0/go.mod h1:xSe+bOZ3asS/ciT91ESQYGhjOql43aBETfvbCzNvad8= +k8s.io/cluster-bootstrap v0.19.0-alpha.2 h1:MHG+0kAEEh4nDQU2iC8NXNILDDIANK12RB8PcAjyej4= +k8s.io/cluster-bootstrap v0.19.0-alpha.2/go.mod h1:n1B0CONbCThI060YxdyBMheo1COXYQwz4Nn4JaQoncY= +k8s.io/code-generator v0.0.0-20190612205613-18da4a14b22b/go.mod h1:G8bQwmHm2eafm5bgtX67XDZQ8CWKSGu9DekI+yN4Y5I= +k8s.io/code-generator v0.0.0-20190912054826-cd179ad6a269/go.mod h1:V5BD6M4CyaN5m+VthcclXWsVcT1Hu+glwa1bi3MIsyE= +k8s.io/code-generator v0.0.0-20190927045949-f81bca4f5e85/go.mod h1:V5BD6M4CyaN5m+VthcclXWsVcT1Hu+glwa1bi3MIsyE= +k8s.io/code-generator v0.17.2/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s= +k8s.io/code-generator v0.17.3/go.mod h1:l8BLVwASXQZTo2xamW5mQNFCe1XPiAesVq7Y1t7PiQQ= +k8s.io/code-generator v0.18.0/go.mod h1:+UHX5rSbxmR8kzS+FAv7um6dtYrZokQvjHpDSYRVkTc= +k8s.io/code-generator v0.19.0-alpha.2/go.mod h1:921XK/cUtrTpn/F0nAhNaFjo8I7Kue1BIEtdxsYFt9I= +k8s.io/component-base v0.0.0-20190918160511-547f6c5d7090/go.mod h1:933PBGtQFJky3TEwYx4aEPZ4IxqhWh3R6DCmzqIn1hA= +k8s.io/component-base v0.0.0-20190918200425-ed2f0867c778/go.mod h1:DFWQCXgXVLiWtzFaS17KxHdlUeUymP7FLxZSkmL9/jU= +k8s.io/component-base v0.0.0-20191003000551-f573d376509c/go.mod h1:VLMr8+SrOC4J3MDbbL7cjBDEPBcwLP9/kv/u8PVUEo4= +k8s.io/component-base v0.17.2/go.mod h1:zMPW3g5aH7cHJpKYQ/ZsGMcgbsA/VyhEugF3QT1awLs= +k8s.io/component-base v0.17.3/go.mod h1:GeQf4BrgelWm64PXkIXiPh/XS0hnO42d9gx9BtbZRp8= +k8s.io/component-base v0.18.0/go.mod h1:u3BCg0z1uskkzrnAKFzulmYaEpZF7XC9Pf/uFyb1v2c= +k8s.io/component-base v0.19.0-alpha.2/go.mod h1:4kR9EHdtjG1uu1t9srFnvr+VoheJzwYwBiNY7zjlCY4= +k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM= +k8s.io/cri-api v0.18.0/go.mod h1:OJtpjDvfsKoLGhvcc0qfygved0S0dGX56IJzPbqTG1s= +k8s.io/cri-api v0.19.0-alpha.2/go.mod h1:49PVLxA+lYKrA6IPBG+C+y5NMOqvdps1PslcanYzTNE= +k8s.io/csi-translation-lib v0.17.3/go.mod h1:FBya8XvGIqDm2/3evLQNxaFXqv/C2UcZa5JgJt6/qqY= +k8s.io/csi-translation-lib v0.18.0/go.mod h1:iF8TE4ACSaPqN1qxmiIjvcU1A8VgkOrpcFGD7Z0hVu0= +k8s.io/csi-translation-lib v0.19.0-alpha.2/go.mod h1:/ACB4N5DPYFaZPSPFYGV1QgpEXoQo/zmWJVKptbIorM= +k8s.io/gengo v0.0.0-20190116091435-f8a0810f38af/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20191010091904-7fa3014cb28f/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20200114144118-36b2048a9120/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20200205140755-e0e292d8aa12/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/heapster v1.2.0-beta.1/go.mod h1:h1uhptVXMwC8xtZBYsPXKVi8fpdlYkTs6k949KozGrM= +k8s.io/helm v2.16.1+incompatible/go.mod h1:LZzlS4LQBHfciFOurYBFkCMTaZ0D1l+p0teMg7TSULI= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.3/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/klog/v2 v2.0.0 h1:Foj74zO6RbjjP4hBEKjnYtjjAhGg4jNynUdYF6fJrok= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/kube-aggregator v0.17.3/go.mod h1:1dMwMFQbmH76RKF0614L7dNenMl3dwnUJuOOyZ3GMXA= +k8s.io/kube-aggregator v0.18.0/go.mod h1:ateewQ5QbjMZF/dihEFXwaEwoA4v/mayRvzfmvb6eqI= +k8s.io/kube-aggregator v0.19.0-alpha.2/go.mod h1:/aqS5yXr+thQwxGVZDff8gnKGpn7ZOPfMKbG/X9Wn+Y= +k8s.io/kube-controller-manager v0.17.3/go.mod h1:22B/TsgVviuCVuNwUrqgyTi5D4AYjMFaK9c8h1oonkY= +k8s.io/kube-controller-manager v0.18.0/go.mod h1:pIRGUrSo+skWzwr5pgWNbgiFWEGSotbamGQpR/gKd5U= +k8s.io/kube-controller-manager v0.19.0-alpha.2/go.mod h1:dXVpebujnggXepHKRtSli1lqKTHu3I9p5hbtZN0rhmc= +k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= +k8s.io/kube-openapi v0.0.0-20190320154901-5e45bb682580/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= +k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf h1:EYm5AW/UUDbnmnI+gK0TJDVK9qPLhM+sRHYanNKw0EQ= +k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/kube-openapi v0.0.0-20190918143330-0270cf2f1c1d/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= +k8s.io/kube-openapi v0.0.0-20200204173128-addea2498afe h1:GOfbcWvX5wW2vcfNch83xYp9SDZjRgAJk+t373yaHKk= +k8s.io/kube-openapi v0.0.0-20200204173128-addea2498afe/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= +k8s.io/kube-openapi v0.0.0-20200403204345-e1beb1bd0f35/go.mod h1:NwPpO8COeh/j9Q9ModsqBxwHcWDo/PmrJOPyquZCC1A= +k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6 h1:Oh3Mzx5pJ+yIumsAD0MOECPVeXsVot0UkiaCGVyfGQY= +k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= +k8s.io/kube-openapi v0.0.0-20200410145947-bcb3869e6f29 h1:NeQXVJ2XFSkRoPzRo8AId01ZER+j8oV4SZADT4iBOXQ= +k8s.io/kube-openapi v0.0.0-20200410145947-bcb3869e6f29/go.mod h1:F+5wygcW0wmRTnM3cOgIqGivxkwSWIWT5YdsDbeAOaU= +k8s.io/kube-openapi v0.0.0-20200413232311-afe0b5e9f729 h1:gO3bDO6nz1phajV/ahvehoz43FWqZyuG8/rLVoEhYLg= +k8s.io/kube-openapi v0.0.0-20200413232311-afe0b5e9f729/go.mod h1:bfCVj+qXcEaE5SCvzBaqpOySr6tuCcpPKqF6HD8nyCw= +k8s.io/kube-proxy v0.17.3/go.mod h1:ds8R8bUYPWtQlspC47Sff7o5aQhWDsv6jpQJATDuqaQ= +k8s.io/kube-proxy v0.18.0/go.mod h1:st3Gcg9wYAd1sn6UMeAs5AHN3R0NOItfB5P6qObKrr8= +k8s.io/kube-proxy v0.19.0-alpha.2/go.mod h1:pQyHt2+878xHPba9Fhmm+wvulhRIlcbgAfWyFzw/KTI= +k8s.io/kube-scheduler v0.17.3/go.mod h1:36HgrrPqzK+rOLTRtDG//b89KjrAZqFI4PXOpdH351M= +k8s.io/kube-scheduler v0.18.0/go.mod h1:GFaNT5Z5/zPZsjXmkGihac2qsT+0u2KIHDgXdFfPHPc= +k8s.io/kube-scheduler v0.19.0-alpha.2/go.mod h1:qpXOM+8tw8kifemkCklH88LpzjXjoFtJ7Uk+T0M90MY= +k8s.io/kube-state-metrics v1.7.2/go.mod h1:U2Y6DRi07sS85rmVPmBFlmv+2peBcL8IWGjM+IjYA/E= +k8s.io/kubectl v0.17.3/go.mod h1:NUn4IBY7f7yCMwSop2HCXlw/MVYP4HJBiUmOR3n9w28= +k8s.io/kubectl v0.18.0/go.mod h1:LOkWx9Z5DXMEg5KtOjHhRiC1fqJPLyCr3KtQgEolCkU= +k8s.io/kubectl v0.19.0-alpha.2/go.mod h1:q4JwL1PWMMazJwtuazDbr3s/6fGyrVO3AnDTFOq27Z8= +k8s.io/kubelet v0.17.3/go.mod h1:Nh8owUHZcUXtnDAtmGnip36Nw+X6c4rbmDQlVyIhwMQ= +k8s.io/kubelet v0.18.0/go.mod h1:1VULM2m7c/ePlIeNOVVK+kkprayDr1RPf1T8oaNaHuQ= +k8s.io/kubelet v0.19.0-alpha.2/go.mod h1:LRBwjyz9Kg8Uzu6y2nelnMRFyzoLhD/3T4PgBVWWCwk= +k8s.io/kubernetes v1.16.0/go.mod h1:nlP2zevWKRGKuaaVbKIwozU0Rjg9leVDXkL4YTtjmVs= +k8s.io/kubernetes v1.16.2/go.mod h1:SmhGgKfQ30imqjFVj8AI+iW+zSyFsswNErKYeTfgoH0= +k8s.io/legacy-cloud-providers v0.17.3/go.mod h1:ujZML5v8efVQxiXXTG+nck7SjP8KhMRjUYNIsoSkYI0= +k8s.io/legacy-cloud-providers v0.18.0/go.mod h1:4Bc9CdZg8wl0mskyhnaXa8DdqLpTUfPEMkw3FZok+H8= +k8s.io/legacy-cloud-providers v0.19.0-alpha.2/go.mod h1:YtvVSX8q9vtjA2mXQrYKXXLN4rADMWrN3CfBkYcFxYI= +k8s.io/metrics v0.17.3/go.mod h1:HEJGy1fhHOjHggW9rMDBJBD3YuGroH3Y1pnIRw9FFaI= +k8s.io/metrics v0.18.0/go.mod h1:8aYTW18koXqjLVKL7Ds05RPMX9ipJZI3mywYvBOxXd4= +k8s.io/metrics v0.19.0-alpha.2/go.mod h1:gNFyVZLslzQtv3GZb/24UFhDHuWMjw596Cd7INID61E= +k8s.io/repo-infra v0.0.0-20181204233714-00fe14e3d1a3/go.mod h1:+G1xBfZDfVFsm1Tj/HNCvg4QqWx8rJ2Fxpqr1rqp/gQ= +k8s.io/sample-apiserver v0.17.0/go.mod h1:SAkguNIe/gJik7VlkFu62oGlWltW3c0mAP9WQYUMEJo= +k8s.io/sample-apiserver v0.17.3/go.mod h1:cn/rvFIttGNqy1v88B5ZlDAbyyqDOoF7JHSwPiqNCNQ= +k8s.io/sample-apiserver v0.18.0/go.mod h1:1RKw7QEixom4PIw/vjUvDgl2QQbuTXbeCUHLlNCzOjg= +k8s.io/sample-apiserver v0.19.0-alpha.2/go.mod h1:e0oCZrTbnK0QNUzsS3o73/oURBTayFQW4GOlDzqzJRs= +k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0= +k8s.io/utils v0.0.0-20190308190857-21c4ce38f2a7/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0= +k8s.io/utils v0.0.0-20190506122338-8fab8cb257d5/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20190801114015-581e00157fb1 h1:+ySTxfHnfzZb9ys375PXNlLhkJPLKgHajBU0N62BDvE= +k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20190809000727-6c36bc71fc4a h1:uy5HAgt4Ha5rEMbhZA+aM1j2cq5LmR6LQ71EYC2sVH4= +k8s.io/utils v0.0.0-20190809000727-6c36bc71fc4a/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20191010214722-8d271d903fe4/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20191030222137-2b95a09bc58d h1:1P0iBJsBzxRmR+dIFnM+Iu4aLxnoa7lBqozW/0uHbT8= +k8s.io/utils v0.0.0-20191030222137-2b95a09bc58d/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo= +k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20200124190032-861946025e34 h1:HjlUD6M0K3P8nRXmr2B9o4F9dUy9TCj/aEpReeyi6+k= +k8s.io/utils v0.0.0-20200124190032-861946025e34/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20200229041039-0a110f9eb7ab h1:I3f2hcBrepGRXI1z4sukzAb8w1R4eqbsHrAsx06LGYM= +k8s.io/utils v0.0.0-20200229041039-0a110f9eb7ab/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20200318093247-d1ab8797c558 h1:yaUqfD7/dWM081lhpYGB+Wi4awn61TY7WcAc6usJMR8= +k8s.io/utils v0.0.0-20200318093247-d1ab8797c558/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20200322164244-327a8059b905 h1:bbO8bYwd3CdH0B2+xhhVShn6HPgpCLmWdYd95I9D/7w= +k8s.io/utils v0.0.0-20200322164244-327a8059b905/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20200327001022-6496210b90e8 h1:6JFbaLjRyBz8K2Jvt+pcT+N3vvwMZfg8MfVENwe9aag= +k8s.io/utils v0.0.0-20200327001022-6496210b90e8/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20200414100711-2df71ebbae66 h1:Ly1Oxdu5p5ZFmiVT71LFgeZETvMfZ1iBIGeOenT2JeM= +k8s.io/utils v0.0.0-20200414100711-2df71ebbae66/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= +modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= +modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= +modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= +modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= +mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I= +mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= +mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo= +mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= +mvdan.cc/unparam v0.0.0-20200314162735-0ac8026f7d06 h1:evGBPL1nfLr4BUt+I0IV8q6P3oOqXhmC+hc8aw7xO8A= +mvdan.cc/unparam v0.0.0-20200314162735-0ac8026f7d06/go.mod h1:A9jtdiT4gKMLUlAQjDEVC18O9SOJ8ZAiqWfq9g3rlj8= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/letsencrypt v0.0.1/go.mod h1:buyQKZ6IXrRnB7TdkHP0RyEybLx18HHyOSoTyoOLqNY= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.7/go.mod h1:PHgbrJT7lCHcxMU+mDHEm+nx46H4zuuHZkDP6icnhu0= +sigs.k8s.io/cluster-api v0.2.6-0.20200213005235-78c565b11999 h1:wzI9iQ/Fkur00DBPKf/W6XD2v58W05qVl7oyR/e5uyY= +sigs.k8s.io/cluster-api v0.2.6-0.20200213005235-78c565b11999/go.mod h1:OCxI4X95BA8R+NL71nRDKYm6Uu87iT9KrCi+OZVb+kE= +sigs.k8s.io/cluster-api v0.2.6-0.20200213155436-e0d1223d6630 h1:QWGkM2snha8sv97N9yQrJ+LEOf4nbSM2jQzeE0I8uZ4= +sigs.k8s.io/cluster-api v0.2.6-0.20200213155436-e0d1223d6630/go.mod h1:OCxI4X95BA8R+NL71nRDKYm6Uu87iT9KrCi+OZVb+kE= +sigs.k8s.io/cluster-api v0.2.7/go.mod h1:Agc72Ra5LMOkQQ2v/Ywv1KUemaYAwvkQ+G59Ym5H8e4= +sigs.k8s.io/cluster-api v0.2.9 h1:PZSuz8hE1u5Y5lZcxi3pDDOc9xxY68uzxVoNGerEXy8= +sigs.k8s.io/cluster-api v0.2.9/go.mod h1:BCw+Pqy1sc8mQ/3d2NZM/f5BApKFCMPsnGvKolvDcA0= +sigs.k8s.io/cluster-api v0.2.10 h1:0vhn1Uy6/j98TM+ihmWRb+lenkmd2VvdbHzrttsClIk= +sigs.k8s.io/cluster-api v0.3.0-rc.0 h1:HPXAdqVgG2/9rX+aup1LViq8YLBcTYT+OTu49GDRRqU= +sigs.k8s.io/cluster-api v0.3.0-rc.0/go.mod h1:adKR0yDa0/3iadC5Iy2Ig0I9EHAtUsBctBW7e6gVJJg= +sigs.k8s.io/cluster-api v0.3.0-rc.0.0.20200216171528-7eead355bcbc h1:1+JQhLCDaFfYgxEaXWMyONgpSLm6mh34Nf8ivgbhoWY= +sigs.k8s.io/cluster-api v0.3.0-rc.0.0.20200216171528-7eead355bcbc/go.mod h1:adKR0yDa0/3iadC5Iy2Ig0I9EHAtUsBctBW7e6gVJJg= +sigs.k8s.io/cluster-api v0.3.2 h1:+oiclIxk1PthGCBbYLEdfZi1u2qFTjK+wwIrtLNlBXA= +sigs.k8s.io/cluster-api v0.3.2/go.mod h1:PkmtV10dmmSmBSpR28c5UxDvWRszTxj+p9KLrb8JVZ8= +sigs.k8s.io/cluster-api v0.3.3 h1:pNuvC6cm67SbxOaPriK8ZfLl1RSjmu/KX2GJiNQH3dU= +sigs.k8s.io/cluster-api v0.3.3/go.mod h1:lgztyeCWCve8YB4DN6QeLR0blNYBvFX9+ATHrxs+zKU= +sigs.k8s.io/controller-runtime v0.3.0/go.mod h1:Cw6PkEg0Sa7dAYovGT4R0tRkGhHXpYijwNxYhAnAZZk= +sigs.k8s.io/controller-runtime v0.4.0 h1:wATM6/m+3w8lj8FXNaO6Fs/rq/vqoOjO1Q116Z9NPsg= +sigs.k8s.io/controller-runtime v0.4.0/go.mod h1:ApC79lpY3PHW9xj/w9pj+lYkLgwAAUZwfXkME1Lajns= +sigs.k8s.io/controller-runtime v0.5.0 h1:CbqIy5fbUX+4E9bpnBFd204YAzRYlM9SWW77BbrcDQo= +sigs.k8s.io/controller-runtime v0.5.0/go.mod h1:REiJzC7Y00U+2YkMbT8wxgrsX5USpXKGhb2sCtAXiT8= +sigs.k8s.io/controller-runtime v0.5.1 h1:TNidCfVoU/cs2i+9xoTcL/l7yhl0bDhYXU0NCG6wmiE= +sigs.k8s.io/controller-runtime v0.5.1/go.mod h1:Uojny7gvg55YLQnEGnPzRE3dC4ik2tRlZJgOUCWXAV4= +sigs.k8s.io/controller-runtime v0.5.2 h1:pyXbUfoTo+HA3jeIfr0vgi+1WtmNh0CwlcnQGLXwsSw= +sigs.k8s.io/controller-runtime v0.5.2/go.mod h1:JZUwSMVbxDupo0lTJSSFP5pimEyxGynROImSsqIOx1A= +sigs.k8s.io/controller-runtime v0.6.0 h1:Fzna3DY7c4BIP6KwfSlrfnj20DJ+SeMBK8HSFvOk9NM= +sigs.k8s.io/controller-runtime v0.6.0/go.mod h1:CpYf5pdNY/B352A1TFLAS2JVSlnGQ5O2cftPHndTroo= +sigs.k8s.io/controller-tools v0.2.4/go.mod h1:m/ztfQNocGYBgTTCmFdnK94uVvgxeZeE3LtJvd/jIzA= +sigs.k8s.io/kind v0.7.1-0.20200303021537-981bd80d3802/go.mod h1:HIZ3PWUezpklcjkqpFbnYOqaqsAE1JeCTEwkgvPLXjk= +sigs.k8s.io/kind v0.8.0 h1:fgG9Mf1B3AJI+au42ISGagzQyGuiw3ztKNWK/tnDbiI= +sigs.k8s.io/kind v0.8.0/go.mod h1:oNKTxUVPYkV9lWzY6CVMNluVq8cBsyq+UgPJdvA3uu4= +sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU= +sigs.k8s.io/structured-merge-diff v0.0.0-20190302045857-e85c7b244fd2/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e h1:4Z09Hglb792X0kfOBBJUPFEyvVfQWrYT/l8h5EKA6JQ= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/structured-merge-diff v0.0.0-20190817042607-6149e4549fca/go.mod h1:IIgPezJWb76P0hotTxzDbWsMYB8APh18qZnxkomBpxA= +sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06 h1:zD2IemQ4LmOcAumeiyDWXKUI2SO0NYDe3H6QGvPOVgU= +sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18= +sigs.k8s.io/structured-merge-diff/v2 v2.0.1/go.mod h1:Wb7vfKAodbKgf6tn1Kl0VvGj7mRH6DGaRcixXEJXTsE= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0 h1:dOmIZBMfhcHS09XZkMyUgkq5trg3/jRyJYFZUiaOp8E= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= +sigs.k8s.io/testing_frameworks v0.1.1 h1:cP2l8fkA3O9vekpy5Ks8mmA0NW/F7yBdXf8brkWhVrs= +sigs.k8s.io/testing_frameworks v0.1.1/go.mod h1:VVBKrHmJ6Ekkfz284YKhQePcdycOzNH9qL6ht1zEr/U= +sigs.k8s.io/testing_frameworks v0.1.2-0.20190130140139-57f07443c2d4/go.mod h1:VVBKrHmJ6Ekkfz284YKhQePcdycOzNH9qL6ht1zEr/U= +sigs.k8s.io/testing_frameworks v0.1.2 h1:vK0+tvjF0BZ/RYFeZ1E6BYBwHJJXhjuZ3TdsEKH+UQM= +sigs.k8s.io/testing_frameworks v0.1.2/go.mod h1:ToQrwSC3s8Xf/lADdZp3Mktcql9CG0UAmdJG9th5i0w= +sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +vbom.ml/util v0.0.0-20160121211510-db5cfe13f5cc/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI= diff --git a/hack/Dockerfile.unit b/hack/Dockerfile.unit new file mode 100644 index 00000000..e4733dc9 --- /dev/null +++ b/hack/Dockerfile.unit @@ -0,0 +1,6 @@ +FROM registry.hub.docker.com/library/golang:1.13 + +WORKDIR /usr/local/kubebuilder +COPY /hack/tools /usr/local/kubebuilder/ +RUN ./install_kustomize.sh && \ + ./install_kubebuilder.sh diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt new file mode 100644 index 00000000..0926592d --- /dev/null +++ b/hack/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +Copyright The Kubernetes 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. +*/ diff --git a/hack/boilerplate/BUILD b/hack/boilerplate/BUILD new file mode 100644 index 00000000..20e91a30 --- /dev/null +++ b/hack/boilerplate/BUILD @@ -0,0 +1,31 @@ +package(default_visibility = ["//visibility:public"]) + +exports_files(glob(["*.txt"])) + +py_test( + name = "boilerplate_test", + srcs = [ + "boilerplate.py", + "boilerplate_test.py", + ], + data = glob([ + "*.txt", + "test/*", + ]), +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [ + ":package-srcs", + "//hack/boilerplate/test:all-srcs", + ], + tags = ["automanaged"], +) diff --git a/hack/boilerplate/boilerplate.Dockerfile.txt b/hack/boilerplate/boilerplate.Dockerfile.txt new file mode 100644 index 00000000..384f325a --- /dev/null +++ b/hack/boilerplate/boilerplate.Dockerfile.txt @@ -0,0 +1,14 @@ +# Copyright YEAR The Kubernetes 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. + diff --git a/hack/boilerplate/boilerplate.Makefile.txt b/hack/boilerplate/boilerplate.Makefile.txt new file mode 100644 index 00000000..384f325a --- /dev/null +++ b/hack/boilerplate/boilerplate.Makefile.txt @@ -0,0 +1,14 @@ +# Copyright YEAR The Kubernetes 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. + diff --git a/hack/boilerplate/boilerplate.bzl.txt b/hack/boilerplate/boilerplate.bzl.txt new file mode 100644 index 00000000..384f325a --- /dev/null +++ b/hack/boilerplate/boilerplate.bzl.txt @@ -0,0 +1,14 @@ +# Copyright YEAR The Kubernetes 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. + diff --git a/hack/boilerplate/boilerplate.generatebzl.txt b/hack/boilerplate/boilerplate.generatebzl.txt new file mode 100644 index 00000000..069e282b --- /dev/null +++ b/hack/boilerplate/boilerplate.generatebzl.txt @@ -0,0 +1,14 @@ +# Copyright The Kubernetes 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. + diff --git a/hack/boilerplate/boilerplate.generatego.txt b/hack/boilerplate/boilerplate.generatego.txt new file mode 100644 index 00000000..b7c650da --- /dev/null +++ b/hack/boilerplate/boilerplate.generatego.txt @@ -0,0 +1,16 @@ +/* +Copyright The Kubernetes 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. +*/ + diff --git a/hack/boilerplate/boilerplate.go.txt b/hack/boilerplate/boilerplate.go.txt new file mode 100644 index 00000000..4b76f1fd --- /dev/null +++ b/hack/boilerplate/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +Copyright YEAR The Kubernetes 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. +*/ diff --git a/hack/boilerplate/boilerplate.py b/hack/boilerplate/boilerplate.py new file mode 100755 index 00000000..cabbaa71 --- /dev/null +++ b/hack/boilerplate/boilerplate.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python + +# Copyright 2015 The Kubernetes 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. + +from __future__ import print_function + +import argparse +import datetime +import difflib +import glob +import os +import re +import sys + +parser = argparse.ArgumentParser() +parser.add_argument( + "filenames", + help="list of files to check, all files if unspecified", + nargs='*') + +rootdir = os.path.dirname(__file__) + "/../../" +rootdir = os.path.abspath(rootdir) +parser.add_argument( + "--rootdir", default=rootdir, help="root directory to examine") + +default_boilerplate_dir = os.path.join(rootdir, "hack/boilerplate") +parser.add_argument( + "--boilerplate-dir", default=default_boilerplate_dir) + +parser.add_argument( + "-v", "--verbose", + help="give verbose output regarding why a file does not pass", + action="store_true") + +args = parser.parse_args() + +verbose_out = sys.stderr if args.verbose else open("/dev/null", "w") + +def get_refs(): + refs = {} + + for path in glob.glob(os.path.join(args.boilerplate_dir, "boilerplate.*.txt")): + extension = os.path.basename(path).split(".")[1] + + ref_file = open(path, 'r') + ref = ref_file.read().splitlines() + ref_file.close() + refs[extension] = ref + + return refs + +def is_generated_file(filename, data, regexs): + for d in skipped_ungenerated_files: + if d in filename: + return False + + p = regexs["generated"] + return p.search(data) + +def file_passes(filename, refs, regexs): + try: + f = open(filename, 'r') + except Exception as exc: + print("Unable to open %s: %s" % (filename, exc), file=verbose_out) + return False + + data = f.read() + f.close() + + # determine if the file is automatically generated + generated = is_generated_file(filename, data, regexs) + + basename = os.path.basename(filename) + extension = file_extension(filename) + if generated: + if extension == "go": + extension = "generatego" + elif extension == "bzl": + extension = "generatebzl" + + if extension != "": + ref = refs[extension] + else: + ref = refs[basename] + + # remove extra content from the top of files + if extension == "go" or extension == "generatego": + p = regexs["go_build_constraints"] + (data, found) = p.subn("", data, 1) + elif extension == "sh": + p = regexs["shebang"] + (data, found) = p.subn("", data, 1) + + data = data.splitlines() + + # if our test file is smaller than the reference it surely fails! + if len(ref) > len(data): + print('File %s smaller than reference (%d < %d)' % + (filename, len(data), len(ref)), + file=verbose_out) + return False + + # trim our file to the same number of lines as the reference file + data = data[:len(ref)] + + p = regexs["year"] + for d in data: + if p.search(d): + if generated: + print('File %s has the YEAR field, but it should not be in generated file' % filename, file=verbose_out) + else: + print('File %s has the YEAR field, but missing the year of date' % filename, file=verbose_out) + return False + + if not generated: + # Replace all occurrences of the regex "2014|2015|2016|2017|2018" with "YEAR" + p = regexs["date"] + for i, d in enumerate(data): + (data[i], found) = p.subn('YEAR', d) + if found != 0: + break + + # if we don't match the reference at this point, fail + if ref != data: + print("Header in %s does not match reference, diff:" % filename, file=verbose_out) + if args.verbose: + print(file=verbose_out) + for line in difflib.unified_diff(ref, data, 'reference', filename, lineterm=''): + print(line, file=verbose_out) + print(file=verbose_out) + return False + + return True + +def file_extension(filename): + return os.path.splitext(filename)[1].split(".")[-1].lower() + +skipped_dirs = ['Godeps', 'third_party', '_gopath', '_output', '.git', 'cluster/env.sh', + "vendor", "test/e2e/generated/bindata.go", "hack/boilerplate/test", + "pkg/kubectl/generated/bindata.go"] + +# list all the files contain 'DO NOT EDIT', but are not generated +skipped_ungenerated_files = ['hack/lib/swagger.sh', 'hack/boilerplate/boilerplate.py'] + +def normalize_files(files): + newfiles = [] + for pathname in files: + if any(x in pathname for x in skipped_dirs): + continue + newfiles.append(pathname) + for i, pathname in enumerate(newfiles): + if not os.path.isabs(pathname): + newfiles[i] = os.path.join(args.rootdir, pathname) + return newfiles + +def get_files(extensions): + files = [] + if len(args.filenames) > 0: + files = args.filenames + else: + for root, dirs, walkfiles in os.walk(args.rootdir): + # don't visit certain dirs. This is just a performance improvement + # as we would prune these later in normalize_files(). But doing it + # cuts down the amount of filesystem walking we do and cuts down + # the size of the file list + for d in skipped_dirs: + if d in dirs: + dirs.remove(d) + + for name in walkfiles: + pathname = os.path.join(root, name) + files.append(pathname) + + files = normalize_files(files) + outfiles = [] + for pathname in files: + basename = os.path.basename(pathname) + extension = file_extension(pathname) + if extension in extensions or basename in extensions: + outfiles.append(pathname) + return outfiles + +def get_dates(): + years = datetime.datetime.now().year + return '(%s)' % '|'.join((str(year) for year in range(2014, years+1))) + +def get_regexs(): + regexs = {} + # Search for "YEAR" which exists in the boilerplate, but shouldn't in the real thing + regexs["year"] = re.compile( 'YEAR' ) + # get_dates return 2014, 2015, 2016, 2017, or 2018 until the current year as a regex like: "(2014|2015|2016|2017|2018)"; + # company holder names can be anything + regexs["date"] = re.compile(get_dates()) + # strip // +build \n\n build constraints + regexs["go_build_constraints"] = re.compile(r"^(// \+build.*\n)+\n", re.MULTILINE) + # strip #!.* from shell scripts + regexs["shebang"] = re.compile(r"^(#!.*\n)\n*", re.MULTILINE) + # Search for generated files + regexs["generated"] = re.compile( 'DO NOT EDIT' ) + return regexs + +def main(): + regexs = get_regexs() + refs = get_refs() + filenames = get_files(refs.keys()) + + for filename in filenames: + if not file_passes(filename, refs, regexs): + print(filename, file=sys.stdout) + + return 0 + +if __name__ == "__main__": + sys.exit(main()) diff --git a/hack/boilerplate/boilerplate.py.txt b/hack/boilerplate/boilerplate.py.txt new file mode 100644 index 00000000..a2e72e59 --- /dev/null +++ b/hack/boilerplate/boilerplate.py.txt @@ -0,0 +1,16 @@ +#!/usr/bin/env python + +# Copyright YEAR The Kubernetes 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. + diff --git a/hack/boilerplate/boilerplate.sh.txt b/hack/boilerplate/boilerplate.sh.txt new file mode 100644 index 00000000..384f325a --- /dev/null +++ b/hack/boilerplate/boilerplate.sh.txt @@ -0,0 +1,14 @@ +# Copyright YEAR The Kubernetes 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. + diff --git a/hack/boilerplate/boilerplate_test.py b/hack/boilerplate/boilerplate_test.py new file mode 100644 index 00000000..b8d5b8e9 --- /dev/null +++ b/hack/boilerplate/boilerplate_test.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python + +# Copyright 2016 The Kubernetes 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. + +import boilerplate +import unittest +import StringIO +import os +import sys + +class TestBoilerplate(unittest.TestCase): + """ + Note: run this test from the hack/boilerplate directory. + + $ python -m unittest boilerplate_test + """ + + def test_boilerplate(self): + os.chdir("test/") + + class Args(object): + def __init__(self): + self.filenames = [] + self.rootdir = "." + self.boilerplate_dir = "../" + self.verbose = True + + # capture stdout + old_stdout = sys.stdout + sys.stdout = StringIO.StringIO() + + boilerplate.args = Args() + ret = boilerplate.main() + + output = sorted(sys.stdout.getvalue().split()) + + sys.stdout = old_stdout + + self.assertEquals( + output, ['././fail.go', '././fail.py']) diff --git a/hack/boilerplate/test/BUILD b/hack/boilerplate/test/BUILD new file mode 100644 index 00000000..869cc05d --- /dev/null +++ b/hack/boilerplate/test/BUILD @@ -0,0 +1,28 @@ +package(default_visibility = ["//visibility:public"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", +) + +go_library( + name = "go_default_library", + srcs = [ + "fail.go", + "pass.go", + ], + importpath = "sigs.k8s.io/cluster-api-provider-aws/hack/boilerplate/test", +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) diff --git a/hack/boilerplate/test/fail.go b/hack/boilerplate/test/fail.go new file mode 100644 index 00000000..16159c5a --- /dev/null +++ b/hack/boilerplate/test/fail.go @@ -0,0 +1,19 @@ +/* +Copyright 2014 The Kubernetes Authors. + +fail + +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 test diff --git a/hack/boilerplate/test/fail.py b/hack/boilerplate/test/fail.py new file mode 100644 index 00000000..cbdd06ff --- /dev/null +++ b/hack/boilerplate/test/fail.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python + +# Copyright 2015 The Kubernetes Authors. +# +# failed +# +# 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. diff --git a/hack/boilerplate/test/pass.go b/hack/boilerplate/test/pass.go new file mode 100644 index 00000000..7508448a --- /dev/null +++ b/hack/boilerplate/test/pass.go @@ -0,0 +1,17 @@ +/* +Copyright 2014 The Kubernetes 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 test diff --git a/hack/boilerplate/test/pass.py b/hack/boilerplate/test/pass.py new file mode 100644 index 00000000..5b7ce29a --- /dev/null +++ b/hack/boilerplate/test/pass.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python + +# Copyright 2015 The Kubernetes 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. + +True diff --git a/hack/ensure-go.sh b/hack/ensure-go.sh new file mode 100755 index 00000000..393bb588 --- /dev/null +++ b/hack/ensure-go.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +# Copyright 2019 The Kubernetes 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. + +set -o errexit +set -o nounset +set -o pipefail + +# Ensure the go tool exists and is a viable version. +verify_go_version() { + if [[ -z "$(command -v go)" ]]; then + cat <&2 + exit 1 +fi + +# Turn colors in this script off by setting the NO_COLOR variable in your +# environment to any value: +# +# $ NO_COLOR=1 test.sh +NO_COLOR=${NO_COLOR:-""} +if [[ -z "${NO_COLOR}" ]]; then + header=$'\e[1;33m' + reset=$'\e[0m' +else + header='' + reset='' +fi + +function header_text { + echo "$header$*$reset" +} + +tmp_root=/tmp + +kb_root_dir=${tmp_root}/kubebuilder + +# Skip fetching and untaring the tools by setting the SKIP_FETCH_TOOLS variable +# in your environment to any value: +# +# $ SKIP_FETCH_TOOLS=1 ./fetch_ext_bins.sh +# +# If you skip fetching tools, this script will use the tools already on your +# machine, but rebuild the kubebuilder and kubebuilder-bin binaries. +SKIP_FETCH_TOOLS=${SKIP_FETCH_TOOLS:-""} + +function prepare_staging_dir { + header_text "preparing staging dir" + + if [[ -z "${SKIP_FETCH_TOOLS}" ]]; then + rm -rf "${kb_root_dir}" + else + rm -f "${kb_root_dir}/kubebuilder/bin/kubebuilder" + rm -f "${kb_root_dir}/kubebuilder/bin/kubebuilder-gen" + rm -f "${kb_root_dir}/kubebuilder/bin/vendor.tar.gz" + fi +} + +# fetch k8s API gen tools and make it available under kb_root_dir/bin. +function fetch_tools { + if [[ -n "$SKIP_FETCH_TOOLS" ]]; then + return 0 + fi + + header_text "fetching tools" + kb_tools_archive_name="kubebuilder-tools-${k8s_version}-${goos}-${goarch}.tar.gz" + kb_tools_download_url="https://storage.googleapis.com/kubebuilder-tools/${kb_tools_archive_name}" + + kb_tools_archive_path="${tmp_root}/${kb_tools_archive_name}" + if [[ ! -f ${kb_tools_archive_path} ]]; then + curl -fsL ${kb_tools_download_url} -o "${kb_tools_archive_path}" + fi + tar -zvxf "${kb_tools_archive_path}" -C "${tmp_root}/" +} + +function setup_envs { + header_text "setting up env vars" + + # Setup env vars + export PATH=/tmp/kubebuilder/bin:$PATH + export TEST_ASSET_KUBECTL=/tmp/kubebuilder/bin/kubectl + export TEST_ASSET_KUBE_APISERVER=/tmp/kubebuilder/bin/kube-apiserver + export TEST_ASSET_ETCD=/tmp/kubebuilder/bin/etcd +} diff --git a/hack/gofmt.sh b/hack/gofmt.sh new file mode 100755 index 00000000..dea1bf2e --- /dev/null +++ b/hack/gofmt.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +set -eux + +IS_CONTAINER=${IS_CONTAINER:-false} +CONTAINER_RUNTIME="${CONTAINER_RUNTIME:-podman}" + +if [ "${IS_CONTAINER}" != "false" ]; then + export XDG_CACHE_HOME=/tmp/.cache + mkdir /tmp/unit + cp -r ./* /tmp/unit + cd /tmp/unit + make fmt +else + "${CONTAINER_RUNTIME}" run --rm \ + --env IS_CONTAINER=TRUE \ + --volume "${PWD}:/go/src/github.com/metal3-io/cluster-api-provider-metal3:ro,z" \ + --entrypoint sh \ + --workdir /go/src/github.com/metal3-io/cluster-api-provider-metal3 \ + registry.hub.docker.com/library/golang:1.12 \ + /go/src/github.com/metal3-io/cluster-api-provider-metal3/hack/gofmt.sh +fi; diff --git a/hack/govet.sh b/hack/govet.sh new file mode 100755 index 00000000..016ddfbe --- /dev/null +++ b/hack/govet.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +set -eux + +IS_CONTAINER=${IS_CONTAINER:-false} +CONTAINER_RUNTIME="${CONTAINER_RUNTIME:-podman}" + +if [ "${IS_CONTAINER}" != "false" ]; then + export XDG_CACHE_HOME=/tmp/.cache + mkdir /tmp/unit + cp -r ./* /tmp/unit + cd /tmp/unit + make vet +else + "${CONTAINER_RUNTIME}" run --rm \ + --env IS_CONTAINER=TRUE \ + --volume "${PWD}:/go/src/github.com/metal3-io/cluster-api-provider-metal3:ro,z" \ + --entrypoint sh \ + --workdir /go/src/github.com/metal3-io/cluster-api-provider-metal3 \ + registry.hub.docker.com/library/golang:1.13 \ + /go/src/github.com/metal3-io/cluster-api-provider-metal3/hack/govet.sh +fi; diff --git a/hack/kind/config.yaml b/hack/kind/config.yaml new file mode 100644 index 00000000..b400b0ee --- /dev/null +++ b/hack/kind/config.yaml @@ -0,0 +1,7 @@ +kind: Cluster +apiVersion: kind.sigs.k8s.io/v1alpha3 +nodes: + - role: control-plane + extraMounts: + - hostPath: /var/run/docker.sock + containerPath: /var/run/docker.sock diff --git a/hack/kind/start_controllers.sh b/hack/kind/start_controllers.sh new file mode 100755 index 00000000..a689bdbb --- /dev/null +++ b/hack/kind/start_controllers.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +set -e + +dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) + +KUBECONFIG="$(kind get kubeconfig-path --name="kind")" +export KUBECONFIG +kubectl cluster-info + +# CAPI +cd ~/go/src/sigs.k8s.io/cluster-api +if [ -n "${BUILD_CAPI}" ]; then + make docker-build +fi +kind load docker-image gcr.io/arvinders-1st-project/cluster-api-controller-amd64:dev +make release-manifests +kubectl apply -f ~/go/src/sigs.k8s.io/cluster-api/out/cluster-api-components.yaml + +# CABPK +cd ~/go/src/sigs.k8s.io/cluster-api-bootstrap-provider-kubeadm +if [ -n "${BUILD_CABPK}" ]; then + make docker-build +fi +kind load docker-image gcr.io/arvinders-1st-project/cluster-api-kubeadm-controller-amd64:dev +make deploy + +# CAPM3 +cd ~/go/src/github.com/metal3-io/cluster-api-provider-metal3 +if [ -n "${BUILD_CAPM3}" ]; then + make docker-build +fi +kind load docker-image controller:dev +make deploy + +# Baremetal-operator +cd ~/go/src/github.com/metal3-io/baremetal-operator +if [ -n "${BUILD_BMO}" ]; then + make docker-build +fi +kind load docker-image baremetal-operator:dev +make deploy + +cd "${dir}" diff --git a/hack/markdownlint.sh b/hack/markdownlint.sh new file mode 100755 index 00000000..b1feb4a4 --- /dev/null +++ b/hack/markdownlint.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +set -eux + +IS_CONTAINER=${IS_CONTAINER:-false} +CONTAINER_RUNTIME="${CONTAINER_RUNTIME:-podman}" + +if [ "${IS_CONTAINER}" != "false" ]; then + TOP_DIR="${1:-.}" + find "${TOP_DIR}" -type d \( -path ./vendor -o -path ./.github \) -prune -o -name '*.md' -exec mdl --style all --warnings {} \+ +else + "${CONTAINER_RUNTIME}" run --rm \ + --env IS_CONTAINER=TRUE \ + --volume "${PWD}:/workdir:ro,z" \ + --entrypoint sh \ + --workdir /workdir \ + registry.hub.docker.com/pipelinecomponents/markdownlint:latest \ + /workdir/hack/markdownlint.sh "${@}" +fi; diff --git a/hack/shellcheck.sh b/hack/shellcheck.sh new file mode 100755 index 00000000..4a96a7a9 --- /dev/null +++ b/hack/shellcheck.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +set -eux + +IS_CONTAINER=${IS_CONTAINER:-false} +CONTAINER_RUNTIME="${CONTAINER_RUNTIME:-podman}" + +if [ "${IS_CONTAINER}" != "false" ]; then + TOP_DIR="${1:-.}" + find "${TOP_DIR}" -path ./vendor -prune -o -name '*.sh' -exec shellcheck -s bash {} \+ +else + "${CONTAINER_RUNTIME}" run --rm \ + --env IS_CONTAINER=TRUE \ + --volume "${PWD}:/workdir:ro,z" \ + --entrypoint sh \ + --workdir /workdir \ + registry.hub.docker.com/koalaman/shellcheck-alpine:stable \ + /workdir/hack/shellcheck.sh "${@}" +fi; diff --git a/hack/tools/go.mod b/hack/tools/go.mod new file mode 100644 index 00000000..8f6e9ccb --- /dev/null +++ b/hack/tools/go.mod @@ -0,0 +1,25 @@ +module sigs.k8s.io/cluster-api/hack/tools + +go 1.12 + +require ( + github.com/golang/mock v1.4.3 + github.com/golangci/golangci-lint v1.17.1 + k8s.io/code-generator v0.17.4 + sigs.k8s.io/controller-tools v0.2.8 + sigs.k8s.io/testing_frameworks v0.1.1 +) + +replace ( + // TODO(vincepri) Remove the following replace directives, needed for golangci-lint until + // https://github.com/golangci/golangci-lint/issues/595 is fixed. + github.com/go-critic/go-critic v0.0.0-20181204210945-1df300866540 => github.com/go-critic/go-critic v0.0.0-20190526074819-1df300866540 + github.com/golangci/errcheck v0.0.0-20181003203344-ef45e06d44b6 => github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 + github.com/golangci/go-tools v0.0.0-20180109140146-af6baa5dc196 => github.com/golangci/go-tools v0.0.0-20190318060251-af6baa5dc196 + github.com/golangci/gofmt v0.0.0-20181105071733-0b8337e80d98 => github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98 + github.com/golangci/gosec v0.0.0-20180901114220-66fb7fc33547 => github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547 + github.com/golangci/ineffassign v0.0.0-20180808204949-42439a7714cc => github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc + github.com/golangci/lint-1 v0.0.0-20180610141402-ee948d087217 => github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217 + github.com/timakin/bodyclose => github.com/golangci/bodyclose v0.0.0-20190714144026-65da19158fa2 + mvdan.cc/unparam v0.0.0-20190124213536-fbb59629db34 => mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34 +) diff --git a/hack/tools/go.sum b/hack/tools/go.sum new file mode 100644 index 00000000..a4211a07 --- /dev/null +++ b/hack/tools/go.sum @@ -0,0 +1,664 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/OpenPeeDeeP/depguard v0.0.0-20180806142446-a69c782687b2 h1:HTOmFEEYrWi4MW5ZKUx6xfeyM10Sx3kQF65xiQJMPYA= +github.com/OpenPeeDeeP/depguard v0.0.0-20180806142446-a69c782687b2/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-critic/go-critic v0.0.0-20190526074819-1df300866540 h1:aTxtKFxZ1TqCCYkrQE6Si8qIm/2+58VSJWurTgEPVHE= +github.com/go-critic/go-critic v0.0.0-20190526074819-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-lintpack/lintpack v0.5.2 h1:DI5mA3+eKdWeJ40nU4d6Wc26qmdG8RCi/btYq0TuRN0= +github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= +github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= +github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= +github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.19.2 h1:A9+F4Dc/MCNB5jibxf6rRvOvR/iFgQdyNx9eIhnGqq0= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.2 h1:o20suLFB4Ri0tuzpWtyHlh7E7HnkqTNLq6aR6WVNS1w= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3 h1:5cxNfTy0UVC3X8JL5ymxzyoUZmo8iZb+jeTWn7tUa8o= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= +github.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCsn0N0ZrQsk= +github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= +github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= +github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.19.2 h1:SStNd1jRcYtfKCN7R0laGNs80WYYvn5CbBjM2sOmCrE= +github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= +github.com/go-openapi/spec v0.19.3 h1:0XRyw8kguri6Yw4SxhsQA/atC88yqrk0+G4YhI2wabc= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= +github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.19.2 h1:jvO6bCMBEilGwMfHhrd61zIID4oIFdwb76V17SM88dE= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= +github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g= +github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4= +github.com/go-toolsmith/astcopy v1.0.0 h1:OMgl1b1MEpjFQ1m5ztEO06rz5CUd3oBv9RF7+DyvdG8= +github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ= +github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= +github.com/go-toolsmith/astequal v1.0.0 h1:4zxD8j3JRFNyLN46lodQuqz3xdKSrur7U/sr0SDS/gQ= +github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= +github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg= +github.com/go-toolsmith/astfmt v1.0.0 h1:A0vDDXt+vsvLEdbMFJAUBI/uTbRw1ffOPnxsILnFL6k= +github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw= +github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU= +github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk= +github.com/go-toolsmith/astp v1.0.0 h1:alXE75TXgcmupDsMK1fRAy0YUzLzqPVvBKoyWV+KPXg= +github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI= +github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks= +github.com/go-toolsmith/pkgload v1.0.0 h1:4DFWWMXVfbcN5So1sBNW9+yeiMqLFGl1wFLTL5R0Tgg= +github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc= +github.com/go-toolsmith/strparse v1.0.0 h1:Vcw78DnpCAKlM20kSbAyO4mPfJn/lyYA4BJUDxe2Jb4= +github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= +github.com/go-toolsmith/typep v1.0.0 h1:zKymWyA1TRYvqYrYDrfEMZULyrhcnGY3x7LDKU2XQaA= +github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= +github.com/gobuffalo/flect v0.1.5 h1:xpKq9ap8MbYfhuPCF0dBH854Gp9CxZjr/IocxELFflo= +github.com/gobuffalo/flect v0.1.5/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= +github.com/gobuffalo/flect v0.2.0 h1:EWCvMGGxOjsgwlWaP+f4+Hh6yrrte7JeFL2S6b+0hdM= +github.com/gobuffalo/flect v0.2.0/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golangci/bodyclose v0.0.0-20190714144026-65da19158fa2 h1:nh/PMhIaxu+h8NOuhOwT2el9Ed08166oitASyNYqQzs= +github.com/golangci/bodyclose v0.0.0-20190714144026-65da19158fa2/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= +github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0= +github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4= +github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM= +github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= +github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 h1:YYWNAGTKWhKpcLLt7aSj/odlKrSrelQwlovBpDuf19w= +github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0= +github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 h1:9kfjN3AdxcbsZBf8NjltjWihK2QfBBBZuv91cMFfDHw= +github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8= +github.com/golangci/go-tools v0.0.0-20190318060251-af6baa5dc196 h1:Y8tnIoL0ZQEnVn+SedetVzw1JRsGvjnOemI+oTFCpow= +github.com/golangci/go-tools v0.0.0-20190318060251-af6baa5dc196/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM= +github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3 h1:pe9JHs3cHHDQgOFXJJdYkK6fLz2PWyYtP4hthoCMvs8= +github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o= +github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee h1:J2XAy40+7yz70uaOiMbNnluTg7gyQhtGqLQncQh+4J8= +github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU= +github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98 h1:0OkFarm1Zy2CjCiDKfK9XHgmc2wbDlRMD2hD8anAJHU= +github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU= +github.com/golangci/golangci-lint v1.17.1 h1:lc8Hf9GPCjIr0hg3S/xhvFT1+Hydass8F1xchr8jkME= +github.com/golangci/golangci-lint v1.17.1/go.mod h1:+5sJSl2h3aly+fpmL2meSP8CaSKua2E4Twi9LPy7b1g= +github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547 h1:fUdgm/BdKvwOHxg5AhNbkNRp2mSy8sxTXyBVs/laQHo= +github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547/go.mod h1:0qUabqiIQgfmlAmulqxyiGkkyF6/tOGSnY2cnPVwrzU= +github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc h1:gLLhTLMk2/SutryVJ6D4VZCU3CUqr8YloG7FPIBWFpI= +github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU= +github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217 h1:En/tZdwhAn0JNwLuXzP3k2RVtMqMmOEK7Yu/g3tmtJE= +github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= +github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA= +github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o= +github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770 h1:EL/O5HGrF7Jaq0yNhBLucz9hTuRzj2LdwGBOaENgxIk= +github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA= +github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21 h1:leSNB7iYzLYSSx3J/s5sVf4Drkc68W2wm4Ixh/mr0us= +github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI= +github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0 h1:HVfrLniijszjS1aiNg8JbBMO2+E1WIQ+j/gL4SQqGPg= +github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4= +github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys= +github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck= +github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3 h1:JVnpOZS+qxli+rgVl98ILOXVNbW+kb5wcxeGx8ShUIw= +github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce h1:xdsDDbiBDQTKASoGEZ+pEmF1OnWuu8AQ9I8iNbHNeno= +github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/magiconair/properties v1.7.6 h1:U+1DqNen04MdEPgFiIwdOUiqZ8qPa37xgogX/sd3+54= +github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63 h1:nTT4s92Dgz2HlrB2NaMgvlfqHH39OgMhA7z3PK7PGD4= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk= +github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238 h1:+MZW2uvHgN8kYvksEN3f7eFL2wpzk0GxmlFsMybWc7E= +github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= +github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663 h1:Ri1EhipkbhWsffPJ3IPlrb4SkTOPa2PfRXp3jchBczw= +github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.1.0 h1:cmiOvKzEunMsAxyhXSzpL5Q1CRKpVv0KQsnAIcSEVYM= +github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI= +github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e h1:MZM7FHLqUHYI0Y/mQAt3d2aYa0SiNms/hFqC9qJYolM= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/sirupsen/logrus v1.0.5 h1:8c8b5uO0zS4X6RPl/sd1ENwSkIc0/H2PaHxE3udaE8I= +github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sourcegraph/go-diff v0.5.1 h1:gO6i5zugwzo1RVTvgvfwCOSVegNuvnNi6bAD1QCmkHs= +github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE= +github.com/spf13/afero v1.1.0 h1:bopulORc2JeYaxfHLvJa5NzxviA9PoWhpiiJkru7Ji4= +github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg= +github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec h1:2ZXvIUGghLpdTVHR1UfvfrzoVlZaE/yOWC5LueIHZig= +github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.0.2 h1:Ncr3ZIuJn322w2k1qmzXDnkLAdQMlJqBa9kfAH+irso= +github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= +github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s= +github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a h1:YX8ljsm6wXlHZO+aRz9Exqr0evNhKRNe5K/gi+zKh4U= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8 h1:1wopBVtVdWnn03fZelqdXTqk7U7zPQCb+T4rbU9ZEoU= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495 h1:I6A9Ag9FpEKOjcKrRNjQkPHawoXIhKyTGfvvjFAiiAk= +golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09 h1:KaQtG+aDELoNmXYas3TVkGNYRuq8JQ1aa7LJt8EXVyo= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc h1:gkKoSkUmnU6bpS/VhkuO27bzQeSA51uaEfbOW5dNb68= +golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872 h1:cGjJzUd8RgBw428LXP65YXni0aiGNA4Bl+ls8SmLOm8= +golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f h1:25KHgbfyiSm6vwQLbM3zZIe1v9p/3ea4Rz+nnM5K/i4= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C17zUDepwGQBb/n+JVg= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190501045030-23463209683d h1:D7DVZUZEUgsSIDTivnUtVeGfN5AvhDIKtdIZAqx0ieE= +golang.org/x/tools v0.0.0-20190501045030-23463209683d/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd h1:7E3PabyysDSEjnaANKBgums/hyvMI/HoHQ50qZEzTrg= +golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac h1:MQEvx39qSf8vyrx3XRaOe+j1UDIzKwkYOVObRgGPVqI= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72 h1:bw9doJza/SFBEweII/rHQh338oozWyiFsBRHtrflcws= +golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485 h1:OB/uP/Puiu5vS5QMRPrXCDWUPb+kt8f1KW8oQzFejQw= +gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e h1:jRyg0XfpwWlhEV8mDfdNGBeSJM2fuyh9Yjrnd8kF2Ts= +gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966 h1:B0J02caTR6tpSJozBJyiAzT6CtBzjclw4pgm9gg8Ys0= +gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.0.0-20190409021203-6e4e0e4f393b h1:aBGgKJUM9Hk/3AE8WaZIApnTxG35kbuQba2w+SXqezo= +k8s.io/api v0.0.0-20190409021203-6e4e0e4f393b/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA= +k8s.io/api v0.17.0 h1:H9d/lw+VkZKEVIUc8F3wgiQ+FUXTTr21M87jXLU7yqM= +k8s.io/api v0.17.0/go.mod h1:npsyOePkeP0CPwyGfXDHxvypiYMJxBWAMpQxCaJ4ZxI= +k8s.io/apiextensions-apiserver v0.0.0-20190409022649-727a075fdec8 h1:q1Qvjzs/iEdXF6A1a8H3AKVFDzJNcJn3nXMs6R6qFtA= +k8s.io/apiextensions-apiserver v0.0.0-20190409022649-727a075fdec8/go.mod h1:IxkesAMoaCRoLrPJdZNZUQp9NfZnzqaVzLhb2VEQzXE= +k8s.io/apiextensions-apiserver v0.17.0 h1:+XgcGxqaMztkbbvsORgCmHIb4uImHKvTjNyu7b8gRnA= +k8s.io/apiextensions-apiserver v0.17.0/go.mod h1:XiIFUakZywkUl54fVXa7QTEHcqQz9HG55nHd1DCoHj8= +k8s.io/apimachinery v0.0.0-20190404173353-6a84e37a896d h1:Jmdtdt1ZnoGfWWIIik61Z7nKYgO3J+swQJtPYsP9wHA= +k8s.io/apimachinery v0.0.0-20190404173353-6a84e37a896d/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0= +k8s.io/apimachinery v0.17.0 h1:xRBnuie9rXcPxUkDizUsGvPf1cnlZCFu210op7J7LJo= +k8s.io/apimachinery v0.17.0/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= +k8s.io/apiserver v0.17.0/go.mod h1:ABM+9x/prjINN6iiffRVNCBR2Wk7uY4z+EtEGZD48cg= +k8s.io/client-go v0.17.0/go.mod h1:TYgR6EUHs6k45hb6KWjVD6jFZvJV4gHDikv/It0xz+k= +k8s.io/code-generator v0.0.0-20190923155300-6206bfaf5c98 h1:00N2FiIVhI98OQHKbF+i3bJohGq50GO4RCHMonKBOHE= +k8s.io/code-generator v0.0.0-20190923155300-6206bfaf5c98/go.mod h1:4MfOrxyyZxxCuenwsdaJRtoSnOP5T13jE2LRYPZ6KeY= +k8s.io/code-generator v0.17.0 h1:y+KWtDWNqlJzJu/kUy8goJZO0X71PGIpAHLX8a0JYk0= +k8s.io/code-generator v0.17.0/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s= +k8s.io/code-generator v0.17.4 h1:C3uu/IvQclEIO4ouUOXuoKWfc4765mYe0uebStg9CaY= +k8s.io/code-generator v0.17.4/go.mod h1:l8BLVwASXQZTo2xamW5mQNFCe1XPiAesVq7Y1t7PiQQ= +k8s.io/component-base v0.17.0/go.mod h1:rKuRAokNMY2nn2A6LP/MiwpoaMRHpfRnrPaUJJj1Yoc= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20190822140433-26a664648505 h1:ZY6yclUKVbZ+SdWnkfY+Je5vrMpKOxmGeKRbsXVmqYM= +k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.2.0 h1:0ElL0OHzF3N+OhoJTL0uca20SxtYt4X4+bzHeqrB83c= +k8s.io/klog v0.2.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf h1:EYm5AW/UUDbnmnI+gK0TJDVK9qPLhM+sRHYanNKw0EQ= +k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo= +k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= +modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= +modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= +modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= +modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= +mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I= +mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= +mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo= +mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= +mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34 h1:duVSyluuJA+u0BnkcLR01smoLrGgDTfWt5c8ODYG8fU= +mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY= +rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/controller-tools v0.2.0 h1:AmQ/0JKBJAjyAiPAkrAf9QW06jkx2lc5hpxMjamsFpw= +sigs.k8s.io/controller-tools v0.2.0/go.mod h1:8t/X+FVWvk6TaBcsa+UKUBbn7GMtvyBKX30SGl4em6Y= +sigs.k8s.io/controller-tools v0.2.5 h1:kH7HKWed9XO42OTxyhUtqyImiefdZV2Q9Jbrytvhf18= +sigs.k8s.io/controller-tools v0.2.5/go.mod h1:+t0Hz6tOhJQCdd7IYO0mNzimmiM9sqMU0021u6UCF2o= +sigs.k8s.io/controller-tools v0.2.8 h1:UmYsnu89dn8/wBhjKL3lkGyaDGRnPDYUx2+iwXRnylA= +sigs.k8s.io/controller-tools v0.2.8/go.mod h1:9VKHPszmf2DHz/QmHkcfZoewO6BL7pPs9uAiBVsaJSE= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18= +sigs.k8s.io/testing_frameworks v0.1.1 h1:cP2l8fkA3O9vekpy5Ks8mmA0NW/F7yBdXf8brkWhVrs= +sigs.k8s.io/testing_frameworks v0.1.1/go.mod h1:VVBKrHmJ6Ekkfz284YKhQePcdycOzNH9qL6ht1zEr/U= +sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4 h1:JPJh2pk3+X4lXAkZIk2RuE/7/FoK9maXw+TNPJhVS/c= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/hack/tools/install_kubebuilder.sh b/hack/tools/install_kubebuilder.sh new file mode 100755 index 00000000..461fed9b --- /dev/null +++ b/hack/tools/install_kubebuilder.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +[[ -f bin/kubebuilder ]] && exit 0 + +version=2.2.0 +arch=amd64 + +mkdir -p ./bin +curl -L -O "https://github.com/kubernetes-sigs/kubebuilder/releases/download/v${version}/kubebuilder_${version}_linux_${arch}.tar.gz" + +tar -zxvf kubebuilder_${version}_linux_${arch}.tar.gz +mv kubebuilder_${version}_linux_${arch}/bin/* bin + +rm kubebuilder_${version}_linux_${arch}.tar.gz +rm -r kubebuilder_${version}_linux_${arch} diff --git a/hack/tools/install_kustomize.sh b/hack/tools/install_kustomize.sh new file mode 100755 index 00000000..3921a608 --- /dev/null +++ b/hack/tools/install_kustomize.sh @@ -0,0 +1,14 @@ +#!/bin/bash -x + +[[ -f bin/kustomize ]] && exit 0 + +version=3.5.4 +arch=amd64 + +mkdir -p ./bin +curl -L -O "https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv${version}/kustomize_v${version}_linux_${arch}.tar.gz" + +tar -xzvf kustomize_v${version}_linux_${arch}.tar.gz +mv kustomize ./bin + +rm kustomize_v${version}_linux_${arch}.tar.gz diff --git a/hack/tools/release/notes.go b/hack/tools/release/notes.go new file mode 100644 index 00000000..40d0411b --- /dev/null +++ b/hack/tools/release/notes.go @@ -0,0 +1,189 @@ +// +build tools + +/* +Copyright 2019 The Kubernetes 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 main + +import ( + "bytes" + "flag" + "fmt" + "os" + "os/exec" + "strings" +) + +/* +This tool prints all the titles of all PRs from previous release to HEAD. +This needs to be run *before* a tag is created. + +Use these as the base of your release notes. +*/ + +const ( + features = ":sparkles: New Features" + bugs = ":bug: Bug Fixes" + documentation = ":book: Documentation" + warning = ":warning: Breaking Changes" + other = ":running: Others" + unknown = ":question: Sort these by hand" +) + +var ( + outputOrder = []string{ + warning, + features, + bugs, + documentation, + other, + unknown, + } + + fromTag = flag.String("from", "", "The tag or commit to start from.") +) + +func main() { + flag.Parse() + os.Exit(run()) +} + +func lastTag() string { + if fromTag != nil && *fromTag != "" { + return *fromTag + } + cmd := exec.Command("git", "describe", "--tags", "--abbrev=0") + out, err := cmd.Output() + if err != nil { + return firstCommit() + } + return string(bytes.TrimSpace(out)) +} + +func firstCommit() string { + cmd := exec.Command("git", "rev-list", "--max-parents=0", "HEAD") + out, err := cmd.Output() + if err != nil { + return "UNKNOWN" + } + return string(bytes.TrimSpace(out)) +} + +func run() int { + lastTag := lastTag() + cmd := exec.Command("git", "rev-list", lastTag+"..HEAD", "--merges", "--pretty=format:%B") + + merges := map[string][]string{ + features: {}, + bugs: {}, + documentation: {}, + warning: {}, + other: {}, + unknown: {}, + } + out, err := cmd.CombinedOutput() + if err != nil { + fmt.Println("Error") + fmt.Println(string(out)) + return 1 + } + + commits := []*commit{} + outLines := strings.Split(string(out), "\n") + for _, line := range outLines { + line = strings.TrimSpace(line) + last := len(commits) - 1 + switch { + case strings.HasPrefix(line, "commit"): + commits = append(commits, &commit{}) + case strings.HasPrefix(line, "Merge"): + commits[last].merge = line + continue + case line == "": + default: + commits[last].body = line + } + } + + for _, c := range commits { + body := strings.TrimSpace(c.body) + var key, prNumber, fork string + switch { + case strings.HasPrefix(body, ":sparkles:"), strings.HasPrefix(body, "✨"): + key = features + body = strings.TrimPrefix(body, ":sparkles:") + body = strings.TrimPrefix(body, "✨") + case strings.HasPrefix(body, ":bug:"), strings.HasPrefix(body, "🐛"): + key = bugs + body = strings.TrimPrefix(body, ":bug:") + body = strings.TrimPrefix(body, "🐛") + case strings.HasPrefix(body, ":book:"), strings.HasPrefix(body, "📖"): + key = documentation + body = strings.TrimPrefix(body, ":book:") + body = strings.TrimPrefix(body, "📖") + case strings.HasPrefix(body, ":running:"), strings.HasPrefix(body, "🏃"): + key = other + body = strings.TrimPrefix(body, ":running:") + body = strings.TrimPrefix(body, "🏃") + case strings.HasPrefix(body, ":warning:"), strings.HasPrefix(body, "⚠️"): + key = warning + body = strings.TrimPrefix(body, ":warning:") + body = strings.TrimPrefix(body, "⚠️") + default: + key = unknown + } + + body = strings.TrimSpace(body) + if body == "" { + continue + } + body = fmt.Sprintf("- %s", body) + fmt.Sscanf(c.merge, "Merge pull request %s from %s", &prNumber, &fork) + merges[key] = append(merges[key], formatMerge(body, prNumber)) + } + + // TODO Turn this into a link (requires knowing the project name + organization) + fmt.Printf("Changes since %v\n---\n", lastTag) + + for _, key := range outputOrder { + mergeslice := merges[key] + if len(mergeslice) > 0 { + fmt.Println("## " + key) + for _, merge := range mergeslice { + fmt.Println(merge) + } + fmt.Println() + } + } + + fmt.Println("The image for this release is: ``.") + fmt.Println("") + fmt.Println("_Thanks to all our contributors!_ 😊") + + return 0 +} + +type commit struct { + merge string + body string +} + +func formatMerge(line, prNumber string) string { + if prNumber == "" { + return line + } + return fmt.Sprintf("%s (%s)", line, prNumber) +} diff --git a/hack/tools/tools.go b/hack/tools/tools.go new file mode 100644 index 00000000..be3bbb02 --- /dev/null +++ b/hack/tools/tools.go @@ -0,0 +1,29 @@ +// +build tools + +/* +Copyright 2019 The Kubernetes 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. +*/ + +// This package imports things required by build scripts, to force `go mod` to see them as dependencies +package tools + +import ( + _ "github.com/golangci/golangci-lint/cmd/golangci-lint" + _ "github.com/jteeuwen/go-bindata/go-bindata" + _ "k8s.io/code-generator/cmd/conversion-gen" + _ "sigs.k8s.io/controller-tools/cmd/controller-gen" + _ "sigs.k8s.io/kustomize/kustomize/v3" + _ "sigs.k8s.io/testing_frameworks/integration" +) diff --git a/hack/unit.sh b/hack/unit.sh new file mode 100755 index 00000000..9422b024 --- /dev/null +++ b/hack/unit.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -eux + +IS_CONTAINER=${IS_CONTAINER:-false} +CONTAINER_RUNTIME="${CONTAINER_RUNTIME:-podman}" + +if [ "${IS_CONTAINER}" != "false" ]; then + export XDG_CACHE_HOME=/tmp/.cache + mkdir /tmp/unit + cp -r ./* /tmp/unit + cp -r /usr/local/kubebuilder/bin /tmp/unit/hack/tools + cd /tmp/unit + make test +else + "${CONTAINER_RUNTIME}" run --rm \ + --env IS_CONTAINER=TRUE \ + --volume "${PWD}:/go/src/github.com/metal3-io/cluster-api-provider-metal3:ro,z" \ + --entrypoint sh \ + --workdir /go/src/github.com/metal3-io/cluster-api-provider-metal3 \ + quay.io/metal3-io/capbm-unit:master \ + /go/src/github.com/metal3-io/cluster-api-provider-metal3/hack/unit.sh "${@}" +fi; diff --git a/hack/verify-boilerplate.sh b/hack/verify-boilerplate.sh new file mode 100755 index 00000000..fc3d3ab7 --- /dev/null +++ b/hack/verify-boilerplate.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +# Copyright 2014 The Kubernetes 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. + +set -o errexit +set -o nounset +set -o pipefail +set -o verbose + +KUBE_ROOT=$(dirname "${BASH_SOURCE[0]}")/.. + +boilerDir="${KUBE_ROOT}/hack/boilerplate" +boiler="${boilerDir}/boilerplate.py" + +files_need_boilerplate=() +while IFS=$'\n' read -r line; do + files_need_boilerplate+=( "$line" ) +done < <("${boiler}" "$@") + +# Run boilerplate check +if [[ ${#files_need_boilerplate[@]} -gt 0 ]]; then + for file in "${files_need_boilerplate[@]}"; do + echo "Boilerplate header is wrong for: ${file}" >&2 + done + + exit 1 +fi diff --git a/ipam/manager_factory.go b/ipam/manager_factory.go new file mode 100644 index 00000000..86662e14 --- /dev/null +++ b/ipam/manager_factory.go @@ -0,0 +1,44 @@ +/* +Copyright 2019 The Kubernetes 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 ipam + +import ( + "github.com/go-logr/logr" + ipamv1 "github.com/metal3-io/ipam/api/v1alpha1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type ManagerFactoryInterface interface { + NewIPPoolManager(*ipamv1.Metal3IPPool, logr.Logger) ( + IPPoolManagerInterface, error, + ) +} + +// ManagerFactory only contains a client +type ManagerFactory struct { + client client.Client +} + +// NewManagerFactory returns a new factory. +func NewManagerFactory(client client.Client) ManagerFactory { + return ManagerFactory{client: client} +} + +// NewIPPoolManager creates a new IPPoolManager +func (f ManagerFactory) NewIPPoolManager(ipPool *ipamv1.Metal3IPPool, metadataLog logr.Logger) (IPPoolManagerInterface, error) { + return NewIPPoolManager(f.client, ipPool, metadataLog) +} diff --git a/ipam/manager_factory_test.go b/ipam/manager_factory_test.go new file mode 100644 index 00000000..0b93bf36 --- /dev/null +++ b/ipam/manager_factory_test.go @@ -0,0 +1,48 @@ +/* +Copyright 2019 The Kubernetes 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 ipam + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + ipamv1 "github.com/metal3-io/ipam/api/v1alpha1" + "k8s.io/klog/klogr" + "sigs.k8s.io/controller-runtime/pkg/client" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +var _ = Describe("Manager factory testing", func() { + var managerClient client.Client + var managerFactory ManagerFactory + clusterLog := klogr.New() + + BeforeEach(func() { + managerClient = fakeclient.NewFakeClientWithScheme(setupScheme()) + managerFactory = NewManagerFactory(managerClient) + }) + + It("returns a manager factory", func() { + Expect(managerFactory.client).To(Equal(managerClient)) + }) + + It("returns an IPPool manager", func() { + _, err := managerFactory.NewIPPoolManager(&ipamv1.Metal3IPPool{}, clusterLog) + Expect(err).NotTo(HaveOccurred()) + }) + +}) diff --git a/ipam/metal3ippool_manager.go b/ipam/metal3ippool_manager.go new file mode 100644 index 00000000..1338f530 --- /dev/null +++ b/ipam/metal3ippool_manager.go @@ -0,0 +1,520 @@ +/* +Copyright 2020 The Kubernetes 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 ipam + +import ( + "context" + "fmt" + "strings" + "net" + "math/big" + + "github.com/go-logr/logr" + ipamv1 "github.com/metal3-io/ipam/api/v1alpha1" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/pointer" + capi "sigs.k8s.io/cluster-api/api/v1alpha3" + "sigs.k8s.io/cluster-api/util/patch" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// IPPoolManagerInterface is an interface for a IPPoolManager +type IPPoolManagerInterface interface { + SetFinalizer() + UnsetFinalizer() + SetClusterOwnerRef(*capi.Cluster) error + UpdateAddresses(context.Context) (int, error) +} + +// IPPoolManager is responsible for performing machine reconciliation +type IPPoolManager struct { + client client.Client + IPPool *ipamv1.Metal3IPPool + Log logr.Logger +} + +// NewIPPoolManager returns a new helper for managing a ipPool object +func NewIPPoolManager(client client.Client, + ipPool *ipamv1.Metal3IPPool, ipPoolLog logr.Logger) (*IPPoolManager, error) { + + return &IPPoolManager{ + client: client, + IPPool: ipPool, + Log: ipPoolLog, + }, nil +} + +// SetFinalizer sets finalizer +func (m *IPPoolManager) SetFinalizer() { + // If the Metal3Machine doesn't have finalizer, add it. + if !Contains(m.IPPool.Finalizers, ipamv1.IPPoolFinalizer) { + m.IPPool.Finalizers = append(m.IPPool.Finalizers, + ipamv1.IPPoolFinalizer, + ) + } +} + +// UnsetFinalizer unsets finalizer +func (m *IPPoolManager) UnsetFinalizer() { + // Remove the finalizer. + m.IPPool.Finalizers = Filter(m.IPPool.Finalizers, + ipamv1.IPPoolFinalizer, + ) +} + +func (m *IPPoolManager) SetClusterOwnerRef(cluster *capi.Cluster) error { + if cluster == nil { + return errors.New("Missing cluster") + } + // Verify that the owner reference is there, if not add it and update object, + // if error requeue. + _, err := findOwnerRefFromList(m.IPPool.OwnerReferences, + cluster.TypeMeta, cluster.ObjectMeta) + if err != nil { + if _, ok := err.(*NotFoundError); !ok { + return err + } + m.IPPool.OwnerReferences, err = setOwnerRefInList( + m.IPPool.OwnerReferences, false, cluster.TypeMeta, + cluster.ObjectMeta, + ) + if err != nil { + return err + } + } + return nil +} + +// RecreateStatus recreates the status if empty +func (m *IPPoolManager) getIndexes(ctx context.Context) (map[ipamv1.IPAddressStr]string, error) { + + m.Log.Info("Fetching Metal3IPAddress objects") + + //start from empty maps + m.IPPool.Status.Allocations = make(map[string]ipamv1.IPAddressStr) + + addresses := make(map[ipamv1.IPAddressStr]string) + + for _, address := range m.IPPool.Spec.PreAllocations { + addresses[address] = "" + } + + // get list of Metal3IPAddress objects + addressObjects := ipamv1.Metal3IPAddressList{} + // without this ListOption, all namespaces would be including in the listing + opts := &client.ListOptions{ + Namespace: m.IPPool.Namespace, + } + + err := m.client.List(ctx, &addressObjects, opts) + if err != nil { + return addresses, err + } + + // Iterate over the Metal3IPAddress objects to find all addresses and objects + for _, addressObject := range addressObjects.Items { + + // If IPPool does not point to this object, discard + if addressObject.Spec.Pool.Name == "" { + continue + } + if addressObject.Spec.Pool.Name != m.IPPool.Name { + continue + } + + // Get the claim Name, if unset use empty string, to still record the + // index being used, to avoid conflicts + claimName := "" + if addressObject.Spec.Claim.Name != "" { + claimName = addressObject.Spec.Claim.Name + } + m.IPPool.Status.Allocations[claimName] = addressObject.Spec.Address + addresses[addressObject.Spec.Address] = claimName + } + m.updateStatusTimestamp() + return addresses, nil +} + +func (m *IPPoolManager) updateStatusTimestamp() { + now := metav1.Now() + m.IPPool.Status.LastUpdated = &now +} + +// UpdateAddresses manages the claims and creates or deletes Metal3IPAddress accordingly. +// It returns the number of current allocations +func (m *IPPoolManager) UpdateAddresses(ctx context.Context) (int, error) { + + addresses, err := m.getIndexes(ctx) + if err != nil { + return 0, err + } + + // get list of Metal3IPClaim objects + addressClaimObjects := ipamv1.Metal3IPClaimList{} + // without this ListOption, all namespaces would be including in the listing + opts := &client.ListOptions{ + Namespace: m.IPPool.Namespace, + } + + err = m.client.List(ctx, &addressClaimObjects, opts) + if err != nil { + return 0, err + } + + // Iterate over the Metal3IPClaim objects to find all addresses and objects + for _, addressClaim := range addressClaimObjects.Items { + // If IPPool does not point to this object, discard + if addressClaim.Spec.Pool.Name != m.IPPool.Name { + continue + } + + if addressClaim.Status.Address != nil && addressClaim.DeletionTimestamp.IsZero() { + continue + } + addresses, err = m.updateAddress(ctx, &addressClaim, addresses) + if err != nil { + return 0, err + } + } + m.updateStatusTimestamp() + return len(addresses), nil +} + +func (m *IPPoolManager) updateAddress(ctx context.Context, + addressClaim *ipamv1.Metal3IPClaim, addresses map[ipamv1.IPAddressStr]string, +) (map[ipamv1.IPAddressStr]string, error) { + helper, err := patch.NewHelper(addressClaim, m.client) + if err != nil { + return addresses, errors.Wrap(err, "failed to init patch helper") + } + // Always patch addressClaim exiting this function so we can persist any changes. + defer func() { + fmt.Printf("\nPatching %v", addressClaim.Name) + err := helper.Patch(ctx, addressClaim) + if err != nil { + m.Log.Info("failed to Patch IPClaim") + } + }() + + addressClaim.Status.ErrorMessage = nil + + if addressClaim.DeletionTimestamp.IsZero() { + addresses, err = m.createAddress(ctx, addressClaim, addresses) + if err != nil { + return addresses, err + } + } else { + addresses, err = m.deleteAddress(ctx, addressClaim, addresses) + if err != nil { + return addresses, err + } + } + return addresses, nil +} + +func (m *IPPoolManager) allocateAddress(addressClaim *ipamv1.Metal3IPClaim, + addresses map[ipamv1.IPAddressStr]string, +) (ipamv1.IPAddressStr, int, *ipamv1.IPAddressStr, error) { + var err error + + // Get pre-allocated addresses + allocatedAddress, ipAllocated := m.IPPool.Spec.PreAllocations[addressClaim.Name] + // If the IP is pre-allocated, the default prefix and gateway are used + prefix := m.IPPool.Spec.Prefix + gateway := m.IPPool.Spec.Gateway + + for _, pool := range m.IPPool.Spec.Pools { + if ipAllocated { + break + } + if pool.Prefix != 0 { + prefix = pool.Prefix + } + if pool.Gateway != nil { + gateway = pool.Gateway + } + index := 0 + for !ipAllocated { + allocatedAddress, err = getIPAddress(pool, index) + fmt.Println(allocatedAddress) + if err != nil { + break + } + index++ + if _, ok := addresses[allocatedAddress]; !ok && allocatedAddress != "" { + ipAllocated = true + } + } + } + if !ipAllocated { + addressClaim.Status.ErrorMessage = pointer.StringPtr("Exhausted IP Pools") + return "", 0, nil, errors.New("Exhausted IP Pools") + } + return allocatedAddress, prefix, gateway, nil +} + +func (m *IPPoolManager) createAddress(ctx context.Context, + addressClaim *ipamv1.Metal3IPClaim, addresses map[ipamv1.IPAddressStr]string, +) (map[ipamv1.IPAddressStr]string, error) { + if !Contains(addressClaim.Finalizers, ipamv1.IPClaimFinalizer) { + addressClaim.Finalizers = append(addressClaim.Finalizers, + ipamv1.IPClaimFinalizer, + ) + } + + if allocatedAddress, ok := m.IPPool.Status.Allocations[addressClaim.Name]; ok { + formatedAddress := strings.Replace( + strings.Replace(string(allocatedAddress), ":", "-", -1), ".", "-", -1, + ) + addressClaim.Status.Address = &corev1.ObjectReference{ + Name: m.IPPool.Spec.NamePrefix + "-" + formatedAddress, + Namespace: m.IPPool.Namespace, + } + return addresses, nil + } + + // Get a new index for this machine + m.Log.Info("Getting address", "Claim", addressClaim.Name) + // Get a new IP for this owner + allocatedAddress, prefix, gateway, err := m.allocateAddress(addressClaim, addresses) + if err != nil { + return addresses, err + } + formatedAddress := strings.Replace( + strings.Replace(string(allocatedAddress), ":", "-", -1), ".", "-", -1, + ) + + // Set the index and Metal3IPAddress names + addressName := m.IPPool.Spec.NamePrefix + "-" + formatedAddress + + m.Log.Info("Address allocated", "Claim", addressClaim.Name, "address", allocatedAddress) + + // Create the Metal3IPAddress object, with an Owner ref to the Metal3Machine + // (curOwnerRef) and to the Metal3IPPool + addressObject := &ipamv1.Metal3IPAddress{ + TypeMeta: metav1.TypeMeta{ + Kind: "Metal3IPAddress", + APIVersion: ipamv1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: addressName, + Namespace: m.IPPool.Namespace, + OwnerReferences: []metav1.OwnerReference{ + metav1.OwnerReference{ + Controller: pointer.BoolPtr(true), + APIVersion: m.IPPool.APIVersion, + Kind: m.IPPool.Kind, + Name: m.IPPool.Name, + UID: m.IPPool.UID, + }, + metav1.OwnerReference{ + APIVersion: addressClaim.APIVersion, + Kind: addressClaim.Kind, + Name: addressClaim.Name, + UID: addressClaim.UID, + }, + }, + Labels: addressClaim.Labels, + }, + Spec: ipamv1.Metal3IPAddressSpec{ + Address: allocatedAddress, + Pool: corev1.ObjectReference{ + Name: m.IPPool.Name, + Namespace: m.IPPool.Namespace, + }, + Claim: corev1.ObjectReference{ + Name: addressClaim.Name, + Namespace: m.IPPool.Namespace, + }, + Prefix: prefix, + Gateway: gateway, + }, + } + + // Create the Metal3IPAddress object. If we get a conflict (that will set + // HasRequeueAfterError), then requeue to retrigger the reconciliation with + // the new state + if err := createObject(m.client, ctx, addressObject); err != nil { + if _, ok := err.(*RequeueAfterError); !ok { + addressClaim.Status.ErrorMessage = pointer.StringPtr("Failed to create associated Metal3IPAddress object") + } + return addresses, err + } + + m.IPPool.Status.Allocations[addressClaim.Name] = allocatedAddress + addresses[allocatedAddress] = addressClaim.Name + + addressClaim.Status.Address = &corev1.ObjectReference{ + Name: addressName, + Namespace: m.IPPool.Namespace, + } + + return addresses, nil +} + +// DeleteDatas deletes old secrets +func (m *IPPoolManager) deleteAddress(ctx context.Context, + addressClaim *ipamv1.Metal3IPClaim, addresses map[ipamv1.IPAddressStr]string, +) (map[ipamv1.IPAddressStr]string, error) { + + m.Log.Info("Deleting Claim", "Metal3IPClaim", addressClaim.Name) + + allocatedAddress, ok := m.IPPool.Status.Allocations[addressClaim.Name] + if ok { + // Try to get the Metal3IPAddress. if it succeeds, delete it + tmpM3Data := &ipamv1.Metal3IPAddress{} + formatedAddress := strings.Replace( + strings.Replace(string(allocatedAddress), ":", "-", -1), ".", "-", -1, + ) + key := client.ObjectKey{ + Name: m.IPPool.Spec.NamePrefix + "-" + formatedAddress, + Namespace: m.IPPool.Namespace, + } + err := m.client.Get(ctx, key, tmpM3Data) + if err != nil && !apierrors.IsNotFound(err) { + addressClaim.Status.ErrorMessage = pointer.StringPtr("Failed to get associated Metal3IPAddress object") + return addresses, err + } else if err == nil { + // Delete the secret with metadata + err = m.client.Delete(ctx, tmpM3Data) + if err != nil && !apierrors.IsNotFound(err) { + addressClaim.Status.ErrorMessage = pointer.StringPtr("Failed to delete associated Metal3IPAddress object") + return addresses, err + } + } + + } + addressClaim.Status.Address = nil + addressClaim.Finalizers = Filter(addressClaim.Finalizers, + ipamv1.IPClaimFinalizer, + ) + + m.Log.Info("Deleted Claim", "Metal3IPClaim", addressClaim.Name) + + if ok { + if _, ok := m.IPPool.Spec.PreAllocations[addressClaim.Name]; !ok { + delete(addresses, allocatedAddress) + } + delete(m.IPPool.Status.Allocations, addressClaim.Name) + } + m.updateStatusTimestamp() + return addresses, nil +} + +// getIPAddress renders the IP address, taking the index, offset and step into +// account, it is IP version agnostic +func getIPAddress(entry ipamv1.IPPool, index int) (ipamv1.IPAddressStr, error) { + + if entry.Start == nil && entry.Subnet == nil { + return "", errors.New("Either Start or Subnet is required for ipAddress") + } + var ip net.IP + var err error + var ipNet *net.IPNet + offset := index + + // If start is given, use it to add the offset + if entry.Start != nil { + var endIP net.IP + if entry.End != nil { + endIP = net.ParseIP(string(*entry.End)) + } + ip, err = addOffsetToIP(net.ParseIP(string(*entry.Start)), endIP, offset) + if err != nil { + return "", err + } + + // Verify that the IP is in the subnet + if entry.Subnet != nil { + _, ipNet, err = net.ParseCIDR(string(*entry.Subnet)) + if err != nil { + return "", err + } + if !ipNet.Contains(ip) { + return "", errors.New("IP address out of bonds") + } + } + + // If it is not given, use the CIDR ip address and increment the offset by 1 + } else { + ip, ipNet, err = net.ParseCIDR(string(*entry.Subnet)) + if err != nil { + return "", err + } + offset++ + ip, err = addOffsetToIP(ip, nil, offset) + if err != nil { + return "", err + } + + // Verify that the ip is in the subnet + if !ipNet.Contains(ip) { + return "", errors.New("IP address out of bonds") + } + } + return ipamv1.IPAddressStr(ip.String()), nil +} + +// addOffsetToIP computes the value of the IP address with the offset. It is +// IP version agnostic +// Note that if the resulting IP address is in the format ::ffff:xxxx:xxxx then +// ip.String will fail to select the correct type of ip +func addOffsetToIP(ip, endIP net.IP, offset int) (net.IP, error) { + ip4 := false + //ip := net.ParseIP(ipString) + if ip.To4() != nil { + ip4 = true + } + + // Create big integers + IPInt := big.NewInt(0) + OffsetInt := big.NewInt(int64(offset)) + + // Transform the ip into an int. (big endian function) + IPInt = IPInt.SetBytes(ip) + + // add the two integers + IPInt = IPInt.Add(IPInt, OffsetInt) + + // return the bytes list + IPBytes := IPInt.Bytes() + + IPBytesLen := len(IPBytes) + + // Verify that the IPv4 or IPv6 fulfills theirs constraints + if (ip4 && IPBytesLen > 6 && IPBytes[4] != 255 && IPBytes[5] != 255) || + (!ip4 && IPBytesLen > 16) { + return nil, errors.New(fmt.Sprintf("IP address overflow for : %s", ip.String())) + } + + //transform the end ip into an Int to compare + if endIP != nil { + endIPInt := big.NewInt(0) + endIPInt = endIPInt.SetBytes(endIP) + // Computed IP is higher than the end IP + if IPInt.Cmp(endIPInt) > 0 { + return nil, errors.New(fmt.Sprintf("IP address out of bonds for : %s", ip.String())) + } + } + + // COpy the output back into an ip + copy(ip[16-IPBytesLen:], IPBytes) + return ip, nil +} diff --git a/ipam/metal3ippool_manager_test.go b/ipam/metal3ippool_manager_test.go new file mode 100644 index 00000000..d9e1eb93 --- /dev/null +++ b/ipam/metal3ippool_manager_test.go @@ -0,0 +1,1157 @@ +/* +Copyright 2019 The Kubernetes 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 ipam + +import ( + "context" + "fmt" + "net" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" + + ipamv1 "github.com/metal3-io/ipam/api/v1alpha1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/klog/klogr" + "k8s.io/utils/pointer" + capi "sigs.k8s.io/cluster-api/api/v1alpha3" + "sigs.k8s.io/controller-runtime/pkg/client" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +var ( + testObjectMeta = metav1.ObjectMeta{ + Name: "abc", + Namespace: "myns", + } + testObjectReference = &corev1.ObjectReference{ + Name: "abc", + } +) + +var _ = Describe("Metal3IPPool manager", func() { + DescribeTable("Test Finalizers", + func(ipPool *ipamv1.Metal3IPPool) { + ipPoolMgr, err := NewIPPoolManager(nil, ipPool, + klogr.New(), + ) + Expect(err).NotTo(HaveOccurred()) + + ipPoolMgr.SetFinalizer() + + Expect(ipPool.ObjectMeta.Finalizers).To(ContainElement( + ipamv1.IPPoolFinalizer, + )) + + ipPoolMgr.UnsetFinalizer() + + Expect(ipPool.ObjectMeta.Finalizers).NotTo(ContainElement( + ipamv1.IPPoolFinalizer, + )) + }, + Entry("No finalizers", &ipamv1.Metal3IPPool{}), + Entry("Additional Finalizers", &ipamv1.Metal3IPPool{ + ObjectMeta: metav1.ObjectMeta{ + Finalizers: []string{"foo"}, + }, + }), + ) + + type testCaseSetClusterOwnerRef struct { + cluster *capi.Cluster + ipPool *ipamv1.Metal3IPPool + expectError bool + } + + DescribeTable("Test SetClusterOwnerRef", + func(tc testCaseSetClusterOwnerRef) { + ipPoolMgr, err := NewIPPoolManager(nil, tc.ipPool, + klogr.New(), + ) + Expect(err).NotTo(HaveOccurred()) + err = ipPoolMgr.SetClusterOwnerRef(tc.cluster) + if tc.expectError { + Expect(err).To(HaveOccurred()) + } else { + Expect(err).NotTo(HaveOccurred()) + _, err := findOwnerRefFromList(tc.ipPool.OwnerReferences, + tc.cluster.TypeMeta, tc.cluster.ObjectMeta) + Expect(err).NotTo(HaveOccurred()) + } + }, + Entry("Cluster missing", testCaseSetClusterOwnerRef{ + expectError: true, + }), + Entry("no previous ownerref", testCaseSetClusterOwnerRef{ + ipPool: &ipamv1.Metal3IPPool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + }, + cluster: &capi.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc-cluster", + }, + }, + }), + Entry("previous ownerref", testCaseSetClusterOwnerRef{ + ipPool: &ipamv1.Metal3IPPool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + OwnerReferences: []metav1.OwnerReference{ + metav1.OwnerReference{ + Name: "def", + }, + }, + }, + }, + cluster: &capi.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc-cluster", + }, + }, + }), + Entry("ownerref present", testCaseSetClusterOwnerRef{ + ipPool: &ipamv1.Metal3IPPool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + OwnerReferences: []metav1.OwnerReference{ + metav1.OwnerReference{ + Name: "def", + }, + metav1.OwnerReference{ + Name: "abc-cluster", + }, + }, + }, + }, + cluster: &capi.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc-cluster", + }, + }, + }), + ) + + type testGetIndexes struct { + ipPool *ipamv1.Metal3IPPool + addresses []*ipamv1.Metal3IPAddress + expectError bool + expectedAddresses map[ipamv1.IPAddressStr]string + expectedAllocations map[string]ipamv1.IPAddressStr + } + + DescribeTable("Test getIndexes", + func(tc testGetIndexes) { + objects := []runtime.Object{} + for _, address := range tc.addresses { + objects = append(objects, address) + } + c := fakeclient.NewFakeClientWithScheme(setupScheme(), objects...) + ipPoolMgr, err := NewIPPoolManager(c, tc.ipPool, + klogr.New(), + ) + Expect(err).NotTo(HaveOccurred()) + + addressMap, err := ipPoolMgr.getIndexes(context.TODO()) + if tc.expectError { + Expect(err).To(HaveOccurred()) + } else { + Expect(err).NotTo(HaveOccurred()) + } + Expect(addressMap).To(Equal(tc.expectedAddresses)) + Expect(tc.ipPool.Status.Allocations).To(Equal(tc.expectedAllocations)) + Expect(tc.ipPool.Status.LastUpdated.IsZero()).To(BeFalse()) + }, + Entry("No addresses", testGetIndexes{ + ipPool: &ipamv1.Metal3IPPool{}, + expectedAddresses: map[ipamv1.IPAddressStr]string{}, + expectedAllocations: map[string]ipamv1.IPAddressStr{}, + }), + Entry("addresses", testGetIndexes{ + ipPool: &ipamv1.Metal3IPPool{ + ObjectMeta: testObjectMeta, + Spec: ipamv1.Metal3IPPoolSpec{ + PreAllocations: map[string]ipamv1.IPAddressStr{ + "bcd": ipamv1.IPAddressStr("bcde"), + }, + }, + }, + addresses: []*ipamv1.Metal3IPAddress{ + &ipamv1.Metal3IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc-0", + Namespace: "myns", + }, + Spec: ipamv1.Metal3IPAddressSpec{ + Address: "abcd1", + Pool: *testObjectReference, + Claim: *testObjectReference, + }, + }, + &ipamv1.Metal3IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bbc-1", + Namespace: "myns", + }, + Spec: ipamv1.Metal3IPAddressSpec{ + Address: "abcd2", + Pool: corev1.ObjectReference{ + Name: "bbc", + Namespace: "myns", + }, + Claim: corev1.ObjectReference{ + Name: "bbc", + Namespace: "myns", + }, + }, + }, + &ipamv1.Metal3IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc-2", + Namespace: "myns", + }, + Spec: ipamv1.Metal3IPAddressSpec{ + Address: "abcd3", + Pool: corev1.ObjectReference{}, + Claim: *testObjectReference, + }, + }, + &ipamv1.Metal3IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc-3", + Namespace: "myns", + }, + Spec: ipamv1.Metal3IPAddressSpec{ + Address: "abcd4", + Pool: corev1.ObjectReference{ + Namespace: "myns", + }, + Claim: corev1.ObjectReference{}, + }, + }, + }, + expectedAddresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("abcd1"): "abc", + ipamv1.IPAddressStr("bcde"): "", + }, + expectedAllocations: map[string]ipamv1.IPAddressStr{ + "abc": ipamv1.IPAddressStr("abcd1"), + }, + }), + ) + + var ipPoolMeta = metav1.ObjectMeta{ + Name: "abc", + Namespace: "myns", + } + + type testCaseUpdateAddresses struct { + ipPool *ipamv1.Metal3IPPool + ipClaims []*ipamv1.Metal3IPClaim + ipAddresses []*ipamv1.Metal3IPAddress + expectRequeue bool + expectError bool + expectedNbAllocations int + expectedAllocations map[string]ipamv1.IPAddressStr + } + + DescribeTable("Test UpdateAddresses", + func(tc testCaseUpdateAddresses) { + objects := []runtime.Object{} + for _, address := range tc.ipAddresses { + objects = append(objects, address) + } + for _, claim := range tc.ipClaims { + objects = append(objects, claim) + } + c := fakeclient.NewFakeClientWithScheme(setupScheme(), objects...) + ipPoolMgr, err := NewIPPoolManager(c, tc.ipPool, + klogr.New(), + ) + Expect(err).NotTo(HaveOccurred()) + + nbAllocations, err := ipPoolMgr.UpdateAddresses(context.TODO()) + if tc.expectRequeue || tc.expectError { + Expect(err).To(HaveOccurred()) + if tc.expectRequeue { + Expect(err).To(BeAssignableToTypeOf(&RequeueAfterError{})) + } else { + Expect(err).NotTo(BeAssignableToTypeOf(&RequeueAfterError{})) + } + } else { + Expect(err).NotTo(HaveOccurred()) + } + Expect(nbAllocations).To(Equal(tc.expectedNbAllocations)) + Expect(tc.ipPool.Status.LastUpdated.IsZero()).To(BeFalse()) + Expect(tc.ipPool.Status.Allocations).To(Equal(tc.expectedAllocations)) + + // get list of Metal3IPAddress objects + addressObjects := ipamv1.Metal3IPClaimList{} + opts := &client.ListOptions{} + err = c.List(context.TODO(), &addressObjects, opts) + Expect(err).NotTo(HaveOccurred()) + + // Iterate over the Metal3IPAddress objects to find all indexes and objects + for _, claim := range addressObjects.Items { + if claim.DeletionTimestamp.IsZero() { + fmt.Printf("%#v", claim) + Expect(claim.Status.Address).NotTo(BeNil()) + } + } + + }, + Entry("No Claims", testCaseUpdateAddresses{ + ipPool: &ipamv1.Metal3IPPool{ + ObjectMeta: ipPoolMeta, + }, + expectedAllocations: map[string]ipamv1.IPAddressStr{}, + }), + Entry("Claim and IP exist", testCaseUpdateAddresses{ + ipPool: &ipamv1.Metal3IPPool{ + ObjectMeta: ipPoolMeta, + Spec: ipamv1.Metal3IPPoolSpec{ + NamePrefix: "abcpref", + }, + }, + ipClaims: []*ipamv1.Metal3IPClaim{ + &ipamv1.Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + Namespace: "myns", + }, + Spec: ipamv1.Metal3IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + }, + }, + &ipamv1.Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abcd", + Namespace: "myns", + }, + Spec: ipamv1.Metal3IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abcd", + Namespace: "myns", + }, + }, + Status: ipamv1.Metal3IPClaimStatus{ + Address: &corev1.ObjectReference{ + Name: "abcpref-192-168-1-12", + Namespace: "myns", + }, + }, + }, + &ipamv1.Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abce", + Namespace: "myns", + }, + Spec: ipamv1.Metal3IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + }, + Status: ipamv1.Metal3IPClaimStatus{ + Address: &corev1.ObjectReference{ + Name: "abcpref-192-168-1-12", + Namespace: "myns", + }, + }, + }, + &ipamv1.Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abcf", + Namespace: "myns", + DeletionTimestamp: &timeNow, + }, + Spec: ipamv1.Metal3IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + }, + Status: ipamv1.Metal3IPClaimStatus{ + Address: &corev1.ObjectReference{ + Name: "abcpref-192-168-1-13", + Namespace: "myns", + }, + }, + }, + }, + ipAddresses: []*ipamv1.Metal3IPAddress{ + &ipamv1.Metal3IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abcpref-192-168-1-11", + Namespace: "myns", + }, + Spec: ipamv1.Metal3IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + Claim: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + Address: ipamv1.IPAddressStr("192.168.1.11"), + Gateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.1")), + Prefix: 24, + }, + }, + &ipamv1.Metal3IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abcpref-192-168-1-12", + Namespace: "myns", + }, + Spec: ipamv1.Metal3IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + Claim: corev1.ObjectReference{ + Name: "abce", + Namespace: "myns", + }, + Address: ipamv1.IPAddressStr("192.168.1.12"), + }, + }, + &ipamv1.Metal3IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abcpref-192-168-1-13", + Namespace: "myns", + }, + Spec: ipamv1.Metal3IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + Claim: corev1.ObjectReference{ + Name: "abcf", + Namespace: "myns", + }, + Address: ipamv1.IPAddressStr("192.168.1.13"), + }, + }, + }, + expectedAllocations: map[string]ipamv1.IPAddressStr{ + "abc": ipamv1.IPAddressStr("192.168.1.11"), + "abce": ipamv1.IPAddressStr("192.168.1.12"), + }, + expectedNbAllocations: 2, + }), + ) + + type testCaseCreateAddresses struct { + ipPool *ipamv1.Metal3IPPool + ipClaim *ipamv1.Metal3IPClaim + ipAddresses []*ipamv1.Metal3IPAddress + addresses map[ipamv1.IPAddressStr]string + expectRequeue bool + expectError bool + expectedIPAddresses []string + expectedAddresses map[ipamv1.IPAddressStr]string + expectedAllocations map[string]ipamv1.IPAddressStr + } + + DescribeTable("Test CreateAddresses", + func(tc testCaseCreateAddresses) { + objects := []runtime.Object{} + for _, address := range tc.ipAddresses { + objects = append(objects, address) + } + c := fakeclient.NewFakeClientWithScheme(setupScheme(), objects...) + ipPoolMgr, err := NewIPPoolManager(c, tc.ipPool, + klogr.New(), + ) + Expect(err).NotTo(HaveOccurred()) + + allocatedMap, err := ipPoolMgr.createAddress(context.TODO(), tc.ipClaim, + tc.addresses, + ) + if tc.expectRequeue || tc.expectError { + Expect(err).To(HaveOccurred()) + if tc.expectRequeue { + Expect(err).To(BeAssignableToTypeOf(&RequeueAfterError{})) + } else { + Expect(err).NotTo(BeAssignableToTypeOf(&RequeueAfterError{})) + } + } else { + Expect(err).NotTo(HaveOccurred()) + } + // get list of Metal3IPAddress objects + addressObjects := ipamv1.Metal3IPAddressList{} + opts := &client.ListOptions{} + err = c.List(context.TODO(), &addressObjects, opts) + Expect(err).NotTo(HaveOccurred()) + + Expect(len(tc.expectedIPAddresses)).To(Equal(len(addressObjects.Items))) + // Iterate over the Metal3IPAddress objects to find all indexes and objects + for _, address := range addressObjects.Items { + Expect(tc.expectedIPAddresses).To(ContainElement(address.Name)) + // TODO add further testing later + } + Expect(len(tc.ipClaim.Finalizers)).To(Equal(1)) + + Expect(allocatedMap).To(Equal(tc.expectedAddresses)) + Expect(tc.ipPool.Status.Allocations).To(Equal(tc.expectedAllocations)) + }, + Entry("Already exists", testCaseCreateAddresses{ + ipPool: &ipamv1.Metal3IPPool{ + ObjectMeta: ipPoolMeta, + Status: ipamv1.Metal3IPPoolStatus{ + Allocations: map[string]ipamv1.IPAddressStr{ + "abc": ipamv1.IPAddressStr("foo-0"), + }, + }, + }, + ipClaim: &ipamv1.Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + }, + expectedAllocations: map[string]ipamv1.IPAddressStr{ + "abc": ipamv1.IPAddressStr("foo-0"), + }, + }), + Entry("Not allocated yet, pre-allocated", testCaseCreateAddresses{ + ipPool: &ipamv1.Metal3IPPool{ + ObjectMeta: ipPoolMeta, + Spec: ipamv1.Metal3IPPoolSpec{ + Pools: []ipamv1.IPPool{ + ipamv1.IPPool{ + Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.20")), + }, + }, + PreAllocations: map[string]ipamv1.IPAddressStr{ + "abc": ipamv1.IPAddressStr("192.168.0.15"), + }, + NamePrefix: "abcpref", + }, + Status: ipamv1.Metal3IPPoolStatus{ + Allocations: map[string]ipamv1.IPAddressStr{}, + }, + }, + addresses: map[ipamv1.IPAddressStr]string{}, + ipClaim: &ipamv1.Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + }, + expectedAllocations: map[string]ipamv1.IPAddressStr{ + "abc": ipamv1.IPAddressStr("192.168.0.15"), + }, + expectedAddresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.15"): "abc", + }, + expectedIPAddresses: []string{"abcpref-192-168-0-15"}, + }), + Entry("Not allocated yet", testCaseCreateAddresses{ + ipPool: &ipamv1.Metal3IPPool{ + ObjectMeta: ipPoolMeta, + Spec: ipamv1.Metal3IPPoolSpec{ + Pools: []ipamv1.IPPool{ + ipamv1.IPPool{ + Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.20")), + }, + }, + NamePrefix: "abcpref", + }, + Status: ipamv1.Metal3IPPoolStatus{ + Allocations: map[string]ipamv1.IPAddressStr{}, + }, + }, + addresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.11"): "bcd", + }, + ipClaim: &ipamv1.Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + }, + expectedAllocations: map[string]ipamv1.IPAddressStr{ + "abc": ipamv1.IPAddressStr("192.168.0.12"), + }, + expectedAddresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.12"): "abc", + ipamv1.IPAddressStr("192.168.0.11"): "bcd", + }, + expectedIPAddresses: []string{"abcpref-192-168-0-12"}, + }), + Entry("Not allocated yet, conflict", testCaseCreateAddresses{ + ipPool: &ipamv1.Metal3IPPool{ + ObjectMeta: ipPoolMeta, + Spec: ipamv1.Metal3IPPoolSpec{ + Pools: []ipamv1.IPPool{ + ipamv1.IPPool{ + Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.20")), + }, + }, + NamePrefix: "abcpref", + }, + Status: ipamv1.Metal3IPPoolStatus{ + Allocations: map[string]ipamv1.IPAddressStr{}, + }, + }, + addresses: map[ipamv1.IPAddressStr]string{}, + ipClaim: &ipamv1.Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + }, + ipAddresses: []*ipamv1.Metal3IPAddress{ + &ipamv1.Metal3IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abcpref-192-168-0-11", + Namespace: "myns", + }, + Spec: ipamv1.Metal3IPAddressSpec{ + Address: "192.168.0.11", + Pool: corev1.ObjectReference{ + Name: "abc", + }, + Claim: corev1.ObjectReference{ + Name: "bcd", + }, + }, + }, + }, + expectedAllocations: map[string]ipamv1.IPAddressStr{}, + expectedAddresses: map[ipamv1.IPAddressStr]string{}, + expectedIPAddresses: []string{"abcpref-192-168-0-11"}, + expectRequeue: true, + }), + Entry("Not allocated yet, exhausted pool", testCaseCreateAddresses{ + ipPool: &ipamv1.Metal3IPPool{ + ObjectMeta: ipPoolMeta, + Spec: ipamv1.Metal3IPPoolSpec{ + Pools: []ipamv1.IPPool{ + ipamv1.IPPool{ + Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.11")), + }, + }, + NamePrefix: "abcpref", + }, + Status: ipamv1.Metal3IPPoolStatus{ + Allocations: map[string]ipamv1.IPAddressStr{}, + }, + }, + addresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.11"): "bcd", + }, + ipClaim: &ipamv1.Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + }, + expectedAllocations: map[string]ipamv1.IPAddressStr{}, + expectedAddresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.11"): "bcd", + }, + expectedIPAddresses: []string{}, + expectError: true, + }), + ) + + type testCaseAllocateAddress struct { + ipPool *ipamv1.Metal3IPPool + ipClaim *ipamv1.Metal3IPClaim + addresses map[ipamv1.IPAddressStr]string + expectedAddress ipamv1.IPAddressStr + expectedPrefix int + expectedGateway *ipamv1.IPAddressStr + expectError bool + } + + DescribeTable("Test AllocateAddress", + func(tc testCaseAllocateAddress) { + ipPoolMgr, err := NewIPPoolManager(nil, tc.ipPool, + klogr.New(), + ) + Expect(err).NotTo(HaveOccurred()) + allocatedAddress, prefix, gateway, err := ipPoolMgr.allocateAddress( + tc.ipClaim, tc.addresses, + ) + if tc.expectError { + Expect(err).To(HaveOccurred()) + return + } else { + Expect(err).NotTo(HaveOccurred()) + } + Expect(allocatedAddress).To(Equal(tc.expectedAddress)) + Expect(prefix).To(Equal(tc.expectedPrefix)) + Expect(*gateway).To(Equal(*tc.expectedGateway)) + }, + Entry("Empty pools", testCaseAllocateAddress{ + ipPool: &ipamv1.Metal3IPPool{ + Spec: ipamv1.Metal3IPPoolSpec{}, + }, + ipClaim: &ipamv1.Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + }, + expectError: true, + }), + Entry("One pool, pre-allocated", testCaseAllocateAddress{ + ipPool: &ipamv1.Metal3IPPool{ + Spec: ipamv1.Metal3IPPoolSpec{ + Pools: []ipamv1.IPPool{ + ipamv1.IPPool{ + Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.20")), + Prefix: 26, + Gateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.1.1")), + }, + }, + PreAllocations: map[string]ipamv1.IPAddressStr{ + "TestRef": ipamv1.IPAddressStr("192.168.0.15"), + }, + Prefix: 24, + Gateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.1")), + }, + }, + ipClaim: &ipamv1.Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + expectedAddress: ipamv1.IPAddressStr("192.168.0.15"), + expectedGateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.1")), + expectedPrefix: 24, + }), + Entry("One pool, with start and existing address", testCaseAllocateAddress{ + ipPool: &ipamv1.Metal3IPPool{ + Spec: ipamv1.Metal3IPPoolSpec{ + Pools: []ipamv1.IPPool{ + ipamv1.IPPool{ + Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.20")), + }, + }, + Prefix: 24, + Gateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.1")), + }, + }, + ipClaim: &ipamv1.Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + addresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.12"): "bcde", + ipamv1.IPAddressStr("192.168.0.11"): "abcd", + }, + expectedAddress: ipamv1.IPAddressStr("192.168.0.13"), + expectedGateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.1")), + expectedPrefix: 24, + }), + Entry("One pool, with subnet and override prefix", testCaseAllocateAddress{ + ipPool: &ipamv1.Metal3IPPool{ + Spec: ipamv1.Metal3IPPoolSpec{ + Pools: []ipamv1.IPPool{ + ipamv1.IPPool{ + Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.20")), + Prefix: 24, + Gateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.1")), + }, + }, + Prefix: 26, + Gateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.1.1")), + }, + }, + ipClaim: &ipamv1.Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + addresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.12"): "bcde", + ipamv1.IPAddressStr("192.168.0.11"): "abcd", + }, + expectedAddress: ipamv1.IPAddressStr("192.168.0.13"), + expectedGateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.1")), + expectedPrefix: 24, + }), + Entry("two pools, with subnet and override prefix", testCaseAllocateAddress{ + ipPool: &ipamv1.Metal3IPPool{ + Spec: ipamv1.Metal3IPPoolSpec{ + Pools: []ipamv1.IPPool{ + ipamv1.IPPool{ + Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.10")), + End: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.10")), + }, + ipamv1.IPPool{ + Subnet: (*ipamv1.IPSubnetStr)(pointer.StringPtr("192.168.1.10/24")), + Prefix: 24, + Gateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.1.1")), + }, + }, + Prefix: 26, + Gateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.2.1")), + }, + }, + ipClaim: &ipamv1.Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + addresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.1.11"): "bcde", + ipamv1.IPAddressStr("192.168.0.10"): "abcd", + }, + expectedAddress: ipamv1.IPAddressStr("192.168.1.12"), + expectedGateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.1.1")), + expectedPrefix: 24, + }), + Entry("Exhausted pools start", testCaseAllocateAddress{ + ipPool: &ipamv1.Metal3IPPool{ + Spec: ipamv1.Metal3IPPoolSpec{ + Pools: []ipamv1.IPPool{ + ipamv1.IPPool{ + Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.10")), + End: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.10")), + }, + }, + Prefix: 24, + Gateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.1")), + }, + }, + ipClaim: &ipamv1.Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + addresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.10"): "abcd", + }, + expectError: true, + }), + Entry("Exhausted pools subnet", testCaseAllocateAddress{ + ipPool: &ipamv1.Metal3IPPool{ + Spec: ipamv1.Metal3IPPoolSpec{ + Pools: []ipamv1.IPPool{ + ipamv1.IPPool{ + Subnet: (*ipamv1.IPSubnetStr)(pointer.StringPtr("192.168.0.0/30")), + }, + }, + Prefix: 24, + Gateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.1")), + }, + }, + ipClaim: &ipamv1.Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + addresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.1"): "abcd", + ipamv1.IPAddressStr("192.168.0.2"): "abcd", + ipamv1.IPAddressStr("192.168.0.3"): "abcd", + }, + expectError: true, + }), + ) + + type testCaseDeleteAddresses struct { + ipPool *ipamv1.Metal3IPPool + ipClaim *ipamv1.Metal3IPClaim + m3addresses []*ipamv1.Metal3IPAddress + addresses map[ipamv1.IPAddressStr]string + expectedAddresses map[ipamv1.IPAddressStr]string + expectedAllocations map[string]ipamv1.IPAddressStr + expectError bool + } + + DescribeTable("Test DeleteAddresses", + func(tc testCaseDeleteAddresses) { + objects := []runtime.Object{} + for _, address := range tc.m3addresses { + objects = append(objects, address) + } + c := fakeclient.NewFakeClientWithScheme(setupScheme(), objects...) + ipPoolMgr, err := NewIPPoolManager(c, tc.ipPool, + klogr.New(), + ) + Expect(err).NotTo(HaveOccurred()) + + allocatedMap, err := ipPoolMgr.deleteAddress(context.TODO(), tc.ipClaim, tc.addresses) + if tc.expectError { + Expect(err).To(HaveOccurred()) + } else { + Expect(err).NotTo(HaveOccurred()) + } + + // get list of Metal3IPAddress objects + addressObjects := ipamv1.Metal3IPAddressList{} + opts := &client.ListOptions{} + err = c.List(context.TODO(), &addressObjects, opts) + Expect(err).NotTo(HaveOccurred()) + + Expect(len(addressObjects.Items)).To(Equal(0)) + + Expect(tc.ipPool.Status.LastUpdated.IsZero()).To(BeFalse()) + Expect(allocatedMap).To(Equal(tc.expectedAddresses)) + Expect(tc.ipPool.Status.Allocations).To(Equal(tc.expectedAllocations)) + Expect(len(tc.ipClaim.Finalizers)).To(Equal(0)) + }, + Entry("Empty IPPool", testCaseDeleteAddresses{ + ipPool: &ipamv1.Metal3IPPool{}, + ipClaim: &ipamv1.Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + }), + Entry("No Deletion needed", testCaseDeleteAddresses{ + ipPool: &ipamv1.Metal3IPPool{}, + ipClaim: &ipamv1.Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + expectedAddresses: map[ipamv1.IPAddressStr]string{ipamv1.IPAddressStr("192.168.0.1"): "abcd"}, + addresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.1"): "abcd", + }, + }), + Entry("Deletion needed, not found", testCaseDeleteAddresses{ + ipPool: &ipamv1.Metal3IPPool{ + Status: ipamv1.Metal3IPPoolStatus{ + Allocations: map[string]ipamv1.IPAddressStr{ + "TestRef": ipamv1.IPAddressStr("192.168.0.1"), + }, + }, + }, + ipClaim: &ipamv1.Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + addresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.1"): "TestRef", + }, + expectedAllocations: map[string]ipamv1.IPAddressStr{}, + expectedAddresses: map[ipamv1.IPAddressStr]string{}, + }), + Entry("Deletion needed", testCaseDeleteAddresses{ + ipPool: &ipamv1.Metal3IPPool{ + Spec: ipamv1.Metal3IPPoolSpec{ + NamePrefix: "abc", + }, + Status: ipamv1.Metal3IPPoolStatus{ + Allocations: map[string]ipamv1.IPAddressStr{ + "TestRef": ipamv1.IPAddressStr("192.168.0.1"), + }, + }, + }, + ipClaim: &ipamv1.Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + Finalizers: []string{ + ipamv1.IPClaimFinalizer, + }, + }, + }, + addresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.1"): "TestRef", + }, + expectedAddresses: map[ipamv1.IPAddressStr]string{}, + expectedAllocations: map[string]ipamv1.IPAddressStr{}, + m3addresses: []*ipamv1.Metal3IPAddress{ + &ipamv1.Metal3IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc-192-168-0-1", + }, + }, + }, + }), + ) + + type testCaseGetIPAddress struct { + ipAddress ipamv1.IPPool + index int + expectError bool + expectedIP ipamv1.IPAddressStr + } + + DescribeTable("Test getIPAddress", + func(tc testCaseGetIPAddress) { + result, err := getIPAddress(tc.ipAddress, tc.index) + if tc.expectError { + Expect(err).To(HaveOccurred()) + } else { + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal(tc.expectedIP)) + } + }, + Entry("Empty Start and Subnet", testCaseGetIPAddress{ + ipAddress: ipamv1.IPPool{}, + index: 1, + expectError: true, + }), + Entry("Start set, no end or subnet", testCaseGetIPAddress{ + ipAddress: ipamv1.IPPool{ + Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.10")), + }, + index: 1, + expectedIP: ipamv1.IPAddressStr("192.168.0.11"), + }), + Entry("Start set, end set, subnet unset", testCaseGetIPAddress{ + ipAddress: ipamv1.IPPool{ + Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.10")), + End: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.100")), + }, + index: 1, + expectedIP: ipamv1.IPAddressStr("192.168.0.11"), + }), + Entry("Start set, end set, subnet unset, out of bound", testCaseGetIPAddress{ + ipAddress: ipamv1.IPPool{ + Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.10")), + End: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.100")), + }, + index: 100, + expectError: true, + }), + Entry("Start set, end unset, subnet set", testCaseGetIPAddress{ + ipAddress: ipamv1.IPPool{ + Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.10")), + Subnet: (*ipamv1.IPSubnetStr)(pointer.StringPtr("192.168.0.0/24")), + }, + index: 1, + expectedIP: ipamv1.IPAddressStr("192.168.0.11"), + }), + Entry("Start set, end unset, subnet set, out of bound", testCaseGetIPAddress{ + ipAddress: ipamv1.IPPool{ + Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.10")), + Subnet: (*ipamv1.IPSubnetStr)(pointer.StringPtr("192.168.0.0/24")), + }, + index: 250, + expectError: true, + }), + Entry("Start set, end unset, subnet empty", testCaseGetIPAddress{ + ipAddress: ipamv1.IPPool{ + Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.10")), + Subnet: (*ipamv1.IPSubnetStr)(pointer.StringPtr("")), + }, + index: 1, + expectError: true, + }), + Entry("subnet empty", testCaseGetIPAddress{ + ipAddress: ipamv1.IPPool{ + Subnet: (*ipamv1.IPSubnetStr)(pointer.StringPtr("")), + }, + index: 1, + expectError: true, + }), + Entry("Start unset, end unset, subnet set", testCaseGetIPAddress{ + ipAddress: ipamv1.IPPool{ + Subnet: (*ipamv1.IPSubnetStr)(pointer.StringPtr("192.168.0.10/24")), + }, + index: 1, + expectedIP: ipamv1.IPAddressStr("192.168.0.12"), + }), + Entry("Start unset, end unset, subnet set, out of bound", testCaseGetIPAddress{ + ipAddress: ipamv1.IPPool{ + Subnet: (*ipamv1.IPSubnetStr)(pointer.StringPtr("192.168.0.10/24")), + }, + index: 250, + expectError: true, + }), + ) + + type testCaseAddOffsetToIP struct { + ip string + endIP string + offset int + expectedIP string + expectError bool + } + + DescribeTable("Test AddOffsetToIP", + func(tc testCaseAddOffsetToIP) { + testIP := net.ParseIP(tc.ip) + testEndIP := net.ParseIP(tc.endIP) + expectedIP := net.ParseIP(tc.expectedIP) + + result, err := addOffsetToIP(testIP, testEndIP, tc.offset) + if tc.expectError { + Expect(err).To(HaveOccurred()) + } else { + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal(expectedIP)) + } + }, + Entry("valid IPv4", testCaseAddOffsetToIP{ + ip: "192.168.0.10", + endIP: "192.168.0.200", + offset: 10, + expectedIP: "192.168.0.20", + }), + Entry("valid IPv4, no end ip", testCaseAddOffsetToIP{ + ip: "192.168.0.10", + offset: 1000, + expectedIP: "192.168.3.242", + }), + Entry("Over bound ipv4", testCaseAddOffsetToIP{ + ip: "192.168.0.10", + endIP: "192.168.0.200", + offset: 1000, + expectError: true, + }), + Entry("error ipv4", testCaseAddOffsetToIP{ + ip: "255.255.255.250", + offset: 10, + expectError: true, + }), + Entry("valid IPv6", testCaseAddOffsetToIP{ + ip: "2001::10", + endIP: "2001::fff0", + offset: 10, + expectedIP: "2001::1A", + }), + Entry("valid IPv6, no end ip", testCaseAddOffsetToIP{ + ip: "2001::10", + offset: 10000, + expectedIP: "2001::2720", + }), + Entry("Over bound ipv6", testCaseAddOffsetToIP{ + ip: "2001::10", + endIP: "2001::00f0", + offset: 10000, + expectError: true, + }), + Entry("error ipv6", testCaseAddOffsetToIP{ + ip: "FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFF0", + offset: 100, + expectError: true, + }), + ) + +}) diff --git a/ipam/mocks/zz_generated.manager_factory.go b/ipam/mocks/zz_generated.manager_factory.go new file mode 100644 index 00000000..690deca7 --- /dev/null +++ b/ipam/mocks/zz_generated.manager_factory.go @@ -0,0 +1,69 @@ +// /* +// Copyright The Kubernetes 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. +// */ +// +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: ./ipam/manager_factory.go + +// Package ipam_mocks is a generated GoMock package. +package ipam_mocks + +import ( + logr "github.com/go-logr/logr" + gomock "github.com/golang/mock/gomock" + v1alpha1 "github.com/metal3-io/ipam/api/v1alpha1" + ipam "github.com/metal3-io/ipam/ipam" + reflect "reflect" +) + +// MockManagerFactoryInterface is a mock of ManagerFactoryInterface interface +type MockManagerFactoryInterface struct { + ctrl *gomock.Controller + recorder *MockManagerFactoryInterfaceMockRecorder +} + +// MockManagerFactoryInterfaceMockRecorder is the mock recorder for MockManagerFactoryInterface +type MockManagerFactoryInterfaceMockRecorder struct { + mock *MockManagerFactoryInterface +} + +// NewMockManagerFactoryInterface creates a new mock instance +func NewMockManagerFactoryInterface(ctrl *gomock.Controller) *MockManagerFactoryInterface { + mock := &MockManagerFactoryInterface{ctrl: ctrl} + mock.recorder = &MockManagerFactoryInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockManagerFactoryInterface) EXPECT() *MockManagerFactoryInterfaceMockRecorder { + return m.recorder +} + +// NewIPPoolManager mocks base method +func (m *MockManagerFactoryInterface) NewIPPoolManager(arg0 *v1alpha1.Metal3IPPool, arg1 logr.Logger) (ipam.IPPoolManagerInterface, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewIPPoolManager", arg0, arg1) + ret0, _ := ret[0].(ipam.IPPoolManagerInterface) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NewIPPoolManager indicates an expected call of NewIPPoolManager +func (mr *MockManagerFactoryInterfaceMockRecorder) NewIPPoolManager(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewIPPoolManager", reflect.TypeOf((*MockManagerFactoryInterface)(nil).NewIPPoolManager), arg0, arg1) +} diff --git a/ipam/mocks/zz_generated.metal3ippool_manager.go b/ipam/mocks/zz_generated.metal3ippool_manager.go new file mode 100644 index 00000000..235e5383 --- /dev/null +++ b/ipam/mocks/zz_generated.metal3ippool_manager.go @@ -0,0 +1,106 @@ +// /* +// Copyright The Kubernetes 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. +// */ +// +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: ./ipam/metal3ippool_manager.go + +// Package ipam_mocks is a generated GoMock package. +package ipam_mocks + +import ( + context "context" + gomock "github.com/golang/mock/gomock" + reflect "reflect" + v1alpha3 "sigs.k8s.io/cluster-api/api/v1alpha3" +) + +// MockIPPoolManagerInterface is a mock of IPPoolManagerInterface interface +type MockIPPoolManagerInterface struct { + ctrl *gomock.Controller + recorder *MockIPPoolManagerInterfaceMockRecorder +} + +// MockIPPoolManagerInterfaceMockRecorder is the mock recorder for MockIPPoolManagerInterface +type MockIPPoolManagerInterfaceMockRecorder struct { + mock *MockIPPoolManagerInterface +} + +// NewMockIPPoolManagerInterface creates a new mock instance +func NewMockIPPoolManagerInterface(ctrl *gomock.Controller) *MockIPPoolManagerInterface { + mock := &MockIPPoolManagerInterface{ctrl: ctrl} + mock.recorder = &MockIPPoolManagerInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockIPPoolManagerInterface) EXPECT() *MockIPPoolManagerInterfaceMockRecorder { + return m.recorder +} + +// SetFinalizer mocks base method +func (m *MockIPPoolManagerInterface) SetFinalizer() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetFinalizer") +} + +// SetFinalizer indicates an expected call of SetFinalizer +func (mr *MockIPPoolManagerInterfaceMockRecorder) SetFinalizer() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetFinalizer", reflect.TypeOf((*MockIPPoolManagerInterface)(nil).SetFinalizer)) +} + +// UnsetFinalizer mocks base method +func (m *MockIPPoolManagerInterface) UnsetFinalizer() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UnsetFinalizer") +} + +// UnsetFinalizer indicates an expected call of UnsetFinalizer +func (mr *MockIPPoolManagerInterfaceMockRecorder) UnsetFinalizer() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnsetFinalizer", reflect.TypeOf((*MockIPPoolManagerInterface)(nil).UnsetFinalizer)) +} + +// SetClusterOwnerRef mocks base method +func (m *MockIPPoolManagerInterface) SetClusterOwnerRef(arg0 *v1alpha3.Cluster) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetClusterOwnerRef", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetClusterOwnerRef indicates an expected call of SetClusterOwnerRef +func (mr *MockIPPoolManagerInterfaceMockRecorder) SetClusterOwnerRef(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetClusterOwnerRef", reflect.TypeOf((*MockIPPoolManagerInterface)(nil).SetClusterOwnerRef), arg0) +} + +// UpdateAddresses mocks base method +func (m *MockIPPoolManagerInterface) UpdateAddresses(arg0 context.Context) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateAddresses", arg0) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateAddresses indicates an expected call of UpdateAddresses +func (mr *MockIPPoolManagerInterfaceMockRecorder) UpdateAddresses(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAddresses", reflect.TypeOf((*MockIPPoolManagerInterface)(nil).UpdateAddresses), arg0) +} diff --git a/ipam/remote/remote.go b/ipam/remote/remote.go new file mode 100644 index 00000000..4c6a23c6 --- /dev/null +++ b/ipam/remote/remote.go @@ -0,0 +1,46 @@ +/* +Copyright 2018 The Kubernetes 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 remote + +import ( + "context" + + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/types" + corev1 "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/tools/clientcmd" + clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3" + kcfg "sigs.k8s.io/cluster-api/util/kubeconfig" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// NewClusterClient creates a new ClusterClient. +func NewClusterClient(ctx context.Context, c client.Client, cluster *clusterv1.Cluster) (corev1.CoreV1Interface, error) { + kubeconfig, err := kcfg.FromSecret(ctx, c, types.NamespacedName{ + Name: cluster.Name, + Namespace: cluster.Namespace, + }) + if err != nil { + return nil, errors.Wrapf(err, "failed to retrieve kubeconfig secret for Cluster %q in namespace %q", + cluster.Name, cluster.Namespace) + } + + restConfig, err := clientcmd.RESTConfigFromKubeConfig(kubeconfig) + if err != nil { + return nil, errors.Wrapf(err, "failed to create client configuration for Cluster %q in namespace %q", + cluster.Name, cluster.Namespace) + } + + return corev1.NewForConfig(restConfig) +} diff --git a/ipam/remote/remote_test.go b/ipam/remote/remote_test.go new file mode 100644 index 00000000..aa46e3d4 --- /dev/null +++ b/ipam/remote/remote_test.go @@ -0,0 +1,118 @@ +/* +Copyright 2019 The Kubernetes 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 remote + +import ( + "context" + "strings" + "testing" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3" + "sigs.k8s.io/cluster-api/util/secret" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +var ( + clusterWithValidKubeConfig = &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test1", + Namespace: "test", + }, + } + + clusterWithInvalidKubeConfig = &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test2-invalid", + Namespace: "test", + }, + } + + clusterWithNoKubeConfig = &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test3", + Namespace: "test", + }, + } + + validKubeConfig = ` +clusters: +- cluster: + server: https://test-cluster-api:6443 + name: test-cluster-api +contexts: +- context: + cluster: test-cluster-api + user: kubernetes-admin + name: kubernetes-admin@test-cluster-api +current-context: kubernetes-admin@test-cluster-api +kind: Config +preferences: {} +users: +- name: kubernetes-admin +` + + validSecret = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test1-kubeconfig", + Namespace: "test", + }, + Data: map[string][]byte{ + secret.KubeconfigDataName: []byte(validKubeConfig), + }, + } + + invalidSecret = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test2-invalid-kubeconfig", + Namespace: "test", + }, + Data: map[string][]byte{ + secret.KubeconfigDataName: []byte("Not valid!!1"), + }, + } +) + +func TestNewClusterClient(t *testing.T) { + t.Run("cluster with valid kubeconfig", func(t *testing.T) { + client := fake.NewFakeClient(validSecret) + c, err := NewClusterClient(context.TODO(), client, clusterWithValidKubeConfig) + if err != nil { + t.Fatalf("Expected no errors, got %v", err) + } + + if c == nil { + t.Fatal("Expected actual client, got nil") + } + }) + + t.Run("cluster with no kubeconfig", func(t *testing.T) { + client := fake.NewFakeClient() + _, err := NewClusterClient(context.TODO(), client, clusterWithNoKubeConfig) + if !strings.Contains(err.Error(), "not found") { + t.Fatalf("Expected not found error, got %v", err) + } + }) + + t.Run("cluster with invalid kubeconfig", func(t *testing.T) { + client := fake.NewFakeClient(invalidSecret) + _, err := NewClusterClient(context.TODO(), client, clusterWithInvalidKubeConfig) + if err == nil || apierrors.IsNotFound(err) { + t.Fatalf("Expected error other than not found, got %v", err) + } + }) + +} diff --git a/ipam/requeue_error.go b/ipam/requeue_error.go new file mode 100644 index 00000000..cdca0f8b --- /dev/null +++ b/ipam/requeue_error.go @@ -0,0 +1,49 @@ +/* +Copyright 2018 The Kubernetes 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 ipam + +import ( + "fmt" + "time" +) + +// HasRequeueAfterError represents that an actuator managed object should +// be requeued for further processing after the given RequeueAfter time has +// passed. +type HasRequeueAfterError interface { + // GetRequeueAfter gets the duration to wait until the managed object is + // requeued for further processing. + GetRequeueAfter() time.Duration +} + +// RequeueAfterError represents that an actuator managed object should be +// requeued for further processing after the given RequeueAfter time has +// passed. +type RequeueAfterError struct { + RequeueAfter time.Duration +} + +// Error implements the error interface +func (e *RequeueAfterError) Error() string { + return fmt.Sprintf("requeue in: %s", e.RequeueAfter) +} + +// GetRequeueAfter gets the duration to wait until the managed object is +// requeued for further processing. +func (e *RequeueAfterError) GetRequeueAfter() time.Duration { + return e.RequeueAfter +} diff --git a/ipam/requeue_error_test.go b/ipam/requeue_error_test.go new file mode 100644 index 00000000..5aed5784 --- /dev/null +++ b/ipam/requeue_error_test.go @@ -0,0 +1,43 @@ +/* +Copyright 2018 The Kubernetes 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 ipam + +import ( + "fmt" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +const ( + RequeueDuration1 = 50 + RequeueDuration2 = 40 +) + +var _ = Describe("Errors testing", func() { + It("returns the correct error", func() { + err := &RequeueAfterError{time.Second * RequeueDuration1} + Expect(err.Error()).To(Equal(fmt.Sprintf("requeue in: %vs", RequeueDuration1))) + }) + + It("Gets the correct duration", func() { + duration, _ := time.ParseDuration(fmt.Sprintf("%vs", RequeueDuration2)) + err := &RequeueAfterError{time.Second * RequeueDuration2} + Expect(err.GetRequeueAfter()).To(Equal(duration)) + }) +}) diff --git a/ipam/suite_test.go b/ipam/suite_test.go new file mode 100644 index 00000000..05c53151 --- /dev/null +++ b/ipam/suite_test.go @@ -0,0 +1,114 @@ +/* +Copyright 2019 The Kubernetes 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 ipam + +import ( + "context" + "path/filepath" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + _ "github.com/go-logr/logr" + ipamv1 "github.com/metal3-io/ipam/api/v1alpha1" + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + _ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +var timeNow = metav1.Now() + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment + +const ( + clusterName = "testCluster" + metal3ClusterName = "testmetal3Cluster" + namespaceName = "testNameSpace" +) + +func TestManagers(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Manager Suite") +} + +var _ = BeforeSuite(func(done Done) { + logf.SetLogger(zap.LoggerTo(GinkgoWriter, true)) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, + } + + var err error + cfg, err = testEnv.Start() + Expect(err).ToNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + + err = ipamv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + err = apiextensionsv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).ToNot(HaveOccurred()) + Expect(k8sClient).ToNot(BeNil()) + err = k8sClient.Create(context.TODO(), &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "myns"}, + }) + Expect(err).NotTo(HaveOccurred()) + + close(done) +}, 60) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).ToNot(HaveOccurred()) +}) + +var bmcOwnerRef = &metav1.OwnerReference{ + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: clusterName, +} + +//----------------------------------- +//------ Helper functions ----------- +//----------------------------------- +func setupScheme() *runtime.Scheme { + s := runtime.NewScheme() + if err := ipamv1.AddToScheme(s); err != nil { + panic(err) + } + return s +} diff --git a/ipam/utils.go b/ipam/utils.go new file mode 100644 index 00000000..0cdb1b6f --- /dev/null +++ b/ipam/utils.go @@ -0,0 +1,163 @@ +/* +Copyright 2019 The Kubernetes 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 ipam + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + // metal3SecretType defines the type of secret created by metal3 + metal3SecretType corev1.SecretType = "infrastructure.cluster.x-k8s.io/secret" +) + +// Filter filters a list for a string. +func Filter(list []string, strToFilter string) (newList []string) { + for _, item := range list { + if item != strToFilter { + newList = append(newList, item) + } + } + return +} + +// Contains returns true if a list contains a string. +func Contains(list []string, strToSearch string) bool { + for _, item := range list { + if item == strToSearch { + return true + } + } + return false +} + +// NotFoundError represents that an object was not found +type NotFoundError struct { +} + +// Error implements the error interface +func (e *NotFoundError) Error() string { + return "Object not found" +} + +func updateObject(cl client.Client, ctx context.Context, obj runtime.Object, opts ...client.UpdateOption) error { + err := cl.Update(ctx, obj.DeepCopyObject(), opts...) + if apierrors.IsConflict(err) { + return &RequeueAfterError{} + } + return err +} + +func createObject(cl client.Client, ctx context.Context, obj runtime.Object, opts ...client.CreateOption) error { + err := cl.Create(ctx, obj.DeepCopyObject(), opts...) + if apierrors.IsAlreadyExists(err) { + return &RequeueAfterError{} + } + return err +} + +func deleteObject(cl client.Client, ctx context.Context, obj runtime.Object, opts ...client.DeleteOption) error { + err := cl.Delete(ctx, obj.DeepCopyObject(), opts...) + if apierrors.IsNotFound(err) { + return nil + } + return err +} + +// DeleteOwnerRefFromList removes the ownerreference to this Metal3 machine +func deleteOwnerRefFromList(refList []metav1.OwnerReference, + objType metav1.TypeMeta, objMeta metav1.ObjectMeta, +) ([]metav1.OwnerReference, error) { + if len(refList) == 0 { + return refList, nil + } + index, err := findOwnerRefFromList(refList, objType, objMeta) + if err != nil { + if _, ok := err.(*NotFoundError); !ok { + return nil, err + } + return refList, nil + } + if len(refList) == 1 { + return []metav1.OwnerReference{}, nil + } + refListLen := len(refList) - 1 + refList[index] = refList[refListLen] + refList, err = deleteOwnerRefFromList(refList[:refListLen-1], objType, objMeta) + if err != nil { + return nil, err + } + return refList, nil +} + +// SetOwnerRef adds an ownerreference to this Metal3 machine +func setOwnerRefInList(refList []metav1.OwnerReference, controller bool, + objType metav1.TypeMeta, objMeta metav1.ObjectMeta, +) ([]metav1.OwnerReference, error) { + index, err := findOwnerRefFromList(refList, objType, objMeta) + if err != nil { + if _, ok := err.(*NotFoundError); !ok { + return nil, err + } + refList = append(refList, metav1.OwnerReference{ + APIVersion: objType.APIVersion, + Kind: objType.Kind, + Name: objMeta.Name, + UID: objMeta.UID, + Controller: pointer.BoolPtr(controller), + }) + } else { + //The UID and the APIVersion might change due to move or version upgrade + refList[index].APIVersion = objType.APIVersion + refList[index].UID = objMeta.UID + refList[index].Controller = pointer.BoolPtr(controller) + } + return refList, nil +} + +func findOwnerRefFromList(refList []metav1.OwnerReference, objType metav1.TypeMeta, + objMeta metav1.ObjectMeta, +) (int, error) { + + for i, curOwnerRef := range refList { + aGV, err := schema.ParseGroupVersion(curOwnerRef.APIVersion) + if err != nil { + return 0, err + } + + bGV, err := schema.ParseGroupVersion(objType.APIVersion) + if err != nil { + return 0, err + } + // not matching on UID since when pivoting it might change + // Not matching on API version as this might change + if curOwnerRef.Name == objMeta.Name && + curOwnerRef.Kind == objType.Kind && + aGV.Group == bGV.Group { + return i, nil + } + } + return 0, &NotFoundError{} +} diff --git a/ipam/utils_test.go b/ipam/utils_test.go new file mode 100644 index 00000000..35c0c193 --- /dev/null +++ b/ipam/utils_test.go @@ -0,0 +1,503 @@ +/* +Copyright 2019 The Kubernetes 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 ipam + +import ( + "context" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" + + ipamv1 "github.com/metal3-io/ipam/api/v1alpha1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ = Describe("Metal3 manager utils", func() { + + type testCaseFilter struct { + TestList []string + TestString string + ExpectedList []string + } + + DescribeTable("Test Filter", + func(tc testCaseFilter) { + resultList := Filter(tc.TestList, tc.TestString) + Expect(resultList).To(Equal(tc.ExpectedList)) + }, + Entry("Absent", testCaseFilter{ + TestList: []string{"abc", "bcd", "def"}, + TestString: "efg", + ExpectedList: []string{"abc", "bcd", "def"}, + }), + Entry("Present in 1", testCaseFilter{ + TestList: []string{"abc", "bcd", "def"}, + TestString: "abc", + ExpectedList: []string{"bcd", "def"}, + }), + Entry("Present in 2", testCaseFilter{ + TestList: []string{"abc", "bcd", "def"}, + TestString: "bcd", + ExpectedList: []string{"abc", "def"}, + }), + Entry("Present in 3", testCaseFilter{ + TestList: []string{"abc", "bcd", "def"}, + TestString: "def", + ExpectedList: []string{"abc", "bcd"}, + }), + ) + + type testCaseContains struct { + TestList []string + TestString string + ExpectedOutput bool + } + + DescribeTable("Test Filter", + func(tc testCaseContains) { + Expect(Contains(tc.TestList, tc.TestString)).To(Equal(tc.ExpectedOutput)) + }, + Entry("Absent", testCaseContains{ + TestList: []string{"abc", "bcd", "def"}, + TestString: "efg", + ExpectedOutput: false, + }), + Entry("Present 1", testCaseContains{ + TestList: []string{"abc", "bcd", "def"}, + TestString: "abc", + ExpectedOutput: true, + }), + Entry("Present 2", testCaseContains{ + TestList: []string{"abc", "bcd", "def"}, + TestString: "bcd", + ExpectedOutput: true, + }), + Entry("Present 3", testCaseContains{ + TestList: []string{"abc", "bcd", "def"}, + TestString: "def", + ExpectedOutput: true, + }), + ) + + Describe("NotFoundError", func() { + It("should return proper message", func() { + err := &NotFoundError{} + Expect(err.Error()).To(Equal("Object not found")) + }) + }) + + type testCaseUpdate struct { + TestObject *ipamv1.Metal3IPClaim + ExistingObject *ipamv1.Metal3IPClaim + ExpectedError bool + } + + DescribeTable("Test Update", + func(tc testCaseUpdate) { + c := k8sClient + if tc.ExistingObject != nil { + err := c.Create(context.TODO(), tc.ExistingObject) + Expect(err).NotTo(HaveOccurred()) + ipPool := ipamv1.Metal3IPClaim{} + err = c.Get(context.TODO(), + client.ObjectKey{ + Name: tc.ExistingObject.Name, + Namespace: tc.ExistingObject.Namespace, + }, + &ipPool, + ) + Expect(err).NotTo(HaveOccurred()) + tc.TestObject.ObjectMeta = ipPool.ObjectMeta + } + obj := tc.TestObject.DeepCopy() + err := updateObject(c, context.TODO(), obj) + if tc.ExpectedError { + Expect(err).To(HaveOccurred()) + Expect(err).NotTo(BeAssignableToTypeOf(&RequeueAfterError{})) + } else { + Expect(err).NotTo(HaveOccurred()) + Expect(obj.Spec).To(Equal(tc.TestObject.Spec)) + Expect(obj.Status).To(Equal(tc.TestObject.Status)) + savedObject := ipamv1.Metal3IPClaim{} + err = c.Get(context.TODO(), + client.ObjectKey{ + Name: tc.TestObject.Name, + Namespace: tc.TestObject.Namespace, + }, + &savedObject, + ) + Expect(err).NotTo(HaveOccurred()) + Expect(savedObject.Spec).To(Equal(tc.TestObject.Spec)) + Expect(savedObject.ResourceVersion).NotTo(Equal(tc.TestObject.ResourceVersion)) + err := updateObject(c, context.TODO(), obj) + Expect(err).To(HaveOccurred()) + Expect(err).To(BeAssignableToTypeOf(&RequeueAfterError{})) + } + err = c.Delete(context.TODO(), tc.TestObject) + if err != nil { + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + } + }, + Entry("Object does not exist", testCaseUpdate{ + TestObject: &ipamv1.Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + Namespace: "myns", + }, + Spec: ipamv1.Metal3IPClaimSpec{ + Pool: corev1.ObjectReference{Name: "abc"}, + }, + Status: ipamv1.Metal3IPClaimStatus{ + Address: &corev1.ObjectReference{Name: "abc"}, + }, + }, + ExistingObject: nil, + ExpectedError: true, + }), + Entry("Object exists", testCaseUpdate{ + TestObject: &ipamv1.Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + Namespace: "myns", + }, + Spec: ipamv1.Metal3IPClaimSpec{ + Pool: corev1.ObjectReference{Name: "abc"}, + }, + Status: ipamv1.Metal3IPClaimStatus{ + Address: &corev1.ObjectReference{Name: "abc"}, + }, + }, + ExistingObject: &ipamv1.Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + Namespace: "myns", + }, + Spec: ipamv1.Metal3IPClaimSpec{ + Pool: corev1.ObjectReference{Name: "abcd"}, + }, + Status: ipamv1.Metal3IPClaimStatus{ + Address: &corev1.ObjectReference{Name: "abcd"}, + }, + }, + ExpectedError: false, + }), + ) + + DescribeTable("Test Create", + func(tc testCaseUpdate) { + c := k8sClient + if tc.ExistingObject != nil { + err := c.Create(context.TODO(), tc.ExistingObject) + Expect(err).NotTo(HaveOccurred()) + } + obj := tc.TestObject.DeepCopy() + err := createObject(c, context.TODO(), obj) + if tc.ExpectedError { + Expect(err).To(HaveOccurred()) + Expect(err).To(BeAssignableToTypeOf(&RequeueAfterError{})) + } else { + Expect(err).NotTo(HaveOccurred()) + Expect(obj.Spec).To(Equal(tc.TestObject.Spec)) + Expect(obj.Status).To(Equal(tc.TestObject.Status)) + savedObject := ipamv1.Metal3IPClaim{} + err = c.Get(context.TODO(), + client.ObjectKey{ + Name: tc.TestObject.Name, + Namespace: tc.TestObject.Namespace, + }, + &savedObject, + ) + Expect(err).NotTo(HaveOccurred()) + Expect(savedObject.Spec).To(Equal(tc.TestObject.Spec)) + } + err = c.Delete(context.TODO(), tc.TestObject) + if err != nil { + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + } + }, + Entry("Object does not exist", testCaseUpdate{ + TestObject: &ipamv1.Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + Namespace: "myns", + }, + Spec: ipamv1.Metal3IPClaimSpec{ + Pool: corev1.ObjectReference{Name: "abc"}, + }, + Status: ipamv1.Metal3IPClaimStatus{ + Address: &corev1.ObjectReference{Name: "abc"}, + }, + }, + ExistingObject: nil, + ExpectedError: false, + }), + Entry("Object exists", testCaseUpdate{ + TestObject: &ipamv1.Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + Namespace: "myns", + }, + Spec: ipamv1.Metal3IPClaimSpec{ + Pool: corev1.ObjectReference{Name: "abc"}, + }, + Status: ipamv1.Metal3IPClaimStatus{ + Address: &corev1.ObjectReference{Name: "abc"}, + }, + }, + ExistingObject: &ipamv1.Metal3IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + Namespace: "myns", + }, + Spec: ipamv1.Metal3IPClaimSpec{ + Pool: corev1.ObjectReference{Name: "abcd"}, + }, + Status: ipamv1.Metal3IPClaimStatus{ + Address: &corev1.ObjectReference{Name: "abcd"}, + }, + }, + ExpectedError: true, + }), + ) + + type testCaseFindOwnerRef struct { + OwnerRefs []metav1.OwnerReference + ExpectError bool + ExpectedIndex int + } + + DescribeTable("Test FindOwnerRef", + func(tc testCaseFindOwnerRef) { + objType := metav1.TypeMeta{ + APIVersion: "abc.com/v1", + Kind: "def", + } + objMeta := metav1.ObjectMeta{ + Name: "ghi", + UID: "adfasdf", + } + index, err := findOwnerRefFromList(tc.OwnerRefs, objType, objMeta) + if tc.ExpectError { + Expect(err).NotTo(BeNil()) + Expect(err).To(BeAssignableToTypeOf(&NotFoundError{})) + } else { + Expect(err).To(BeNil()) + Expect(index).To(BeEquivalentTo(tc.ExpectedIndex)) + } + }, + Entry("Empty list", testCaseFindOwnerRef{ + OwnerRefs: []metav1.OwnerReference{}, + ExpectError: true, + }), + Entry("Absent", testCaseFindOwnerRef{ + OwnerRefs: []metav1.OwnerReference{ + metav1.OwnerReference{ + APIVersion: "abc.com/v1", + Kind: "def", + Name: "ghij", + UID: "adfasdf", + }, + }, + ExpectError: true, + }), + Entry("Present 0", testCaseFindOwnerRef{ + OwnerRefs: []metav1.OwnerReference{ + metav1.OwnerReference{ + APIVersion: "abc.com/v1", + Kind: "def", + Name: "ghi", + UID: "adfasdf", + }, + }, + ExpectError: false, + ExpectedIndex: 0, + }), + Entry("Present 1", testCaseFindOwnerRef{ + OwnerRefs: []metav1.OwnerReference{ + metav1.OwnerReference{ + APIVersion: "abc.com/v1", + Kind: "def", + Name: "ghij", + UID: "adfasdf", + }, + metav1.OwnerReference{ + APIVersion: "abc.com/v1", + Kind: "def", + Name: "ghi", + UID: "adfasdf", + }, + }, + ExpectError: false, + ExpectedIndex: 1, + }), + Entry("Present but different versions", testCaseFindOwnerRef{ + OwnerRefs: []metav1.OwnerReference{ + metav1.OwnerReference{ + APIVersion: "abc.com/v2", + Kind: "def", + Name: "ghi", + UID: "adfasdf", + }, + }, + ExpectError: false, + ExpectedIndex: 0, + }), + Entry("Wrong group", testCaseFindOwnerRef{ + OwnerRefs: []metav1.OwnerReference{ + metav1.OwnerReference{ + APIVersion: "abc.co/v1", + Kind: "def", + Name: "ghi", + UID: "adfasdf", + }, + }, + ExpectError: true, + }), + ) + + type testCaseOwnerRef struct { + OwnerRefs []metav1.OwnerReference + Controller bool + } + + DescribeTable("Test DeleteOwnerRef", + func(tc testCaseOwnerRef) { + objType := metav1.TypeMeta{ + APIVersion: "abc.com/v1", + Kind: "def", + } + objMeta := metav1.ObjectMeta{ + Name: "ghi", + UID: "adfasdf", + } + refList, err := deleteOwnerRefFromList(tc.OwnerRefs, objType, objMeta) + Expect(err).To(BeNil()) + _, err = findOwnerRefFromList(refList, objType, objMeta) + Expect(err).NotTo(BeNil()) + }, + Entry("Empty list", testCaseOwnerRef{ + OwnerRefs: []metav1.OwnerReference{}, + }), + Entry("Absent", testCaseOwnerRef{ + OwnerRefs: []metav1.OwnerReference{ + metav1.OwnerReference{ + APIVersion: "abc.com/v1", + Kind: "def", + Name: "ghij", + UID: "adfasdf", + }, + }, + }), + Entry("Present 0", testCaseOwnerRef{ + OwnerRefs: []metav1.OwnerReference{ + metav1.OwnerReference{ + APIVersion: "abc.com/v1", + Kind: "def", + Name: "ghi", + UID: "adfasdf", + }, + }, + }), + Entry("Present 1", testCaseOwnerRef{ + OwnerRefs: []metav1.OwnerReference{ + metav1.OwnerReference{ + APIVersion: "abc.com/v1", + Kind: "def", + Name: "ghij", + UID: "adfasdf", + }, + metav1.OwnerReference{ + APIVersion: "abc.com/v1", + Kind: "def", + Name: "ghij", + UID: "adfasdf", + }, + }, + }), + Entry("Present", testCaseOwnerRef{ + OwnerRefs: []metav1.OwnerReference{ + metav1.OwnerReference{ + APIVersion: "abc.com/v1", + Kind: "def", + Name: "ghi", + UID: "adfasdf", + }, + }, + }), + ) + + DescribeTable("Test SetOwnerRef", + func(tc testCaseOwnerRef) { + objType := metav1.TypeMeta{ + APIVersion: "abc.com/v1", + Kind: "def", + } + objMeta := metav1.ObjectMeta{ + Name: "ghi", + UID: "adfasdf", + } + refList, err := setOwnerRefInList(tc.OwnerRefs, tc.Controller, objType, objMeta) + Expect(err).To(BeNil()) + index, err := findOwnerRefFromList(refList, objType, objMeta) + Expect(err).To(BeNil()) + Expect(*refList[index].Controller).To(BeEquivalentTo(tc.Controller)) + }, + Entry("Empty list", testCaseOwnerRef{ + OwnerRefs: []metav1.OwnerReference{}, + }), + Entry("Absent", testCaseOwnerRef{ + OwnerRefs: []metav1.OwnerReference{ + metav1.OwnerReference{ + APIVersion: "abc.com/v1", + Kind: "def", + Name: "ghij", + UID: "adfasdf", + }, + }, + }), + Entry("Present 0", testCaseOwnerRef{ + OwnerRefs: []metav1.OwnerReference{ + metav1.OwnerReference{ + APIVersion: "abc.com/v1", + Kind: "def", + Name: "ghi", + UID: "adfasdf", + }, + }, + }), + Entry("Present 1", testCaseOwnerRef{ + OwnerRefs: []metav1.OwnerReference{ + metav1.OwnerReference{ + APIVersion: "abc.com/v1", + Kind: "def", + Name: "ghij", + UID: "adfasdf", + }, + metav1.OwnerReference{ + APIVersion: "abc.com/v1", + Kind: "def", + Name: "ghi", + UID: "adfasdf", + }, + }, + }), + ) +}) diff --git a/main.go b/main.go new file mode 100644 index 00000000..a53c4492 --- /dev/null +++ b/main.go @@ -0,0 +1,312 @@ +/* +Copyright 2019 The Kubernetes 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 main + +import ( + "flag" + "fmt" + "os" + "time" + + bmoapis "github.com/metal3-io/baremetal-operator/pkg/apis" + infrav1alpha2 "github.com/metal3-io/cluster-api-provider-metal3/api/v1alpha2" + infrav1alpha3 "github.com/metal3-io/cluster-api-provider-metal3/api/v1alpha3" + ipamv1 "github.com/metal3-io/ipam/api/v1alpha1" + "github.com/metal3-io/cluster-api-provider-metal3/baremetal" + capm3remote "github.com/metal3-io/cluster-api-provider-metal3/baremetal/remote" + "github.com/metal3-io/cluster-api-provider-metal3/controllers" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" + "k8s.io/client-go/kubernetes/scheme" + _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" + "k8s.io/client-go/rest" + "k8s.io/klog" + "k8s.io/klog/klogr" + clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + // +kubebuilder:scaffold:imports +) + +var ( + myscheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") + waitForMetal3Controller = false + metricsAddr string + enableLeaderElection bool + syncPeriod time.Duration + webhookPort int + healthAddr string + watchNamespace string +) + +func init() { + _ = scheme.AddToScheme(myscheme) + _ = infrav1.AddToScheme(myscheme) + _ = clusterv1.AddToScheme(myscheme) + _ = bmoapis.AddToScheme(myscheme) + _ = infrav1alpha2.AddToScheme(myscheme) + _ = infrav1alpha3.AddToScheme(myscheme) + // +kubebuilder:scaffold:scheme +} + +func main() { + klog.InitFlags(nil) + flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") + flag.BoolVar(&enableLeaderElection, "enable-leader-election", false, + "Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.") + flag.StringVar(&watchNamespace, "namespace", "", + "Namespace that the controller watches to reconcile CAPM3 objects. If unspecified, the controller watches for CAPM3 objects across all namespaces.") + flag.DurationVar(&syncPeriod, "sync-period", 10*time.Minute, + "The minimum interval at which watched resources are reconciled (e.g. 15m)") + flag.IntVar(&webhookPort, "webhook-port", 0, + "Webhook Server port (set to 0 to disable)") + flag.StringVar(&healthAddr, "health-addr", ":9440", + "The address the health endpoint binds to.") + flag.Parse() + + ctrl.SetLogger(klogr.New()) + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: myscheme, + MetricsBindAddress: metricsAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "controller-leader-election-capm3", + SyncPeriod: &syncPeriod, + Port: webhookPort, + HealthProbeBindAddress: healthAddr, + Namespace: watchNamespace, + }) + if err != nil { + setupLog.Error(err, "unable to start manager") + os.Exit(1) + } + + if waitForMetal3Controller { + err = waitForAPIs(ctrl.GetConfigOrDie()) + if err != nil { + setupLog.Error(err, "unable to discover required APIs") + os.Exit(1) + } + } + + setupChecks(mgr) + setupReconcilers(mgr) + setupWebhooks(mgr) + + // +kubebuilder:scaffold:builder + setupLog.Info("starting manager") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } +} + +func waitForAPIs(cfg *rest.Config) error { + c, err := discovery.NewDiscoveryClientForConfig(cfg) + if err != nil { + return err + } + + metal3GV := schema.GroupVersion{ + Group: "metal3.io", + Version: "v1alpha1", + } + + for { + err = discovery.ServerSupportsVersion(c, metal3GV) + if err != nil { + setupLog.Info(fmt.Sprintf("Waiting for API group %v to be available: %v", metal3GV, err)) + time.Sleep(time.Second * 10) + continue + } + setupLog.Info(fmt.Sprintf("Found API group %v", metal3GV)) + break + } + + return nil +} + +func setupChecks(mgr ctrl.Manager) { + if err := mgr.AddReadyzCheck("ping", healthz.Ping); err != nil { + setupLog.Error(err, "unable to create ready check") + os.Exit(1) + } + + if err := mgr.AddHealthzCheck("ping", healthz.Ping); err != nil { + setupLog.Error(err, "unable to create health check") + os.Exit(1) + } +} + +func setupReconcilers(mgr ctrl.Manager) { + if webhookPort != 0 { + return + } + if err := (&controllers.Metal3MachineReconciler{ + Client: mgr.GetClient(), + ManagerFactory: baremetal.NewManagerFactory(mgr.GetClient()), + Log: ctrl.Log.WithName("controllers").WithName("Metal3Machine"), + CapiClientGetter: capm3remote.NewClusterClient, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Metal3MachineReconciler") + os.Exit(1) + } + + if err := (&controllers.Metal3ClusterReconciler{ + Client: mgr.GetClient(), + ManagerFactory: baremetal.NewManagerFactory(mgr.GetClient()), + Log: ctrl.Log.WithName("controllers").WithName("Metal3Cluster"), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Metal3ClusterReconciler") + os.Exit(1) + } + + if err := (&controllers.Metal3DataTemplateReconciler{ + Client: mgr.GetClient(), + ManagerFactory: baremetal.NewManagerFactory(mgr.GetClient()), + Log: ctrl.Log.WithName("controllers").WithName("Metal3DataTemplate"), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Metal3DataTemplateReconciler") + os.Exit(1) + } + + if err := (&controllers.Metal3DataReconciler{ + Client: mgr.GetClient(), + ManagerFactory: baremetal.NewManagerFactory(mgr.GetClient()), + Log: ctrl.Log.WithName("controllers").WithName("Metal3Data"), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Metal3DataReconciler") + os.Exit(1) + } + + if err := (&controllers.Metal3IPPoolReconciler{ + Client: mgr.GetClient(), + ManagerFactory: baremetal.NewManagerFactory(mgr.GetClient()), + Log: ctrl.Log.WithName("controllers").WithName("Metal3IPPool"), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Metal3IPPoolReconciler") + os.Exit(1) + } +} + +func setupWebhooks(mgr ctrl.Manager) { + if webhookPort == 0 { + return + } + if err := (&infrav1alpha2.Metal3Cluster{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Metal3Cluster") + os.Exit(1) + } + if err := (&infrav1alpha3.Metal3Cluster{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Metal3Cluster") + os.Exit(1) + } + if err := (&infrav1.Metal3Cluster{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Metal3Cluster") + os.Exit(1) + } + + if err := (&infrav1alpha2.Metal3ClusterList{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Metal3ClusterList") + os.Exit(1) + } + + if err := (&infrav1alpha3.Metal3ClusterList{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Metal3ClusterList") + os.Exit(1) + } + + if err := (&infrav1alpha2.Metal3Machine{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Metal3Machine") + os.Exit(1) + } + + if err := (&infrav1alpha3.Metal3Machine{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Metal3Machine") + os.Exit(1) + } + if err := (&infrav1.Metal3Machine{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Metal3Machine") + os.Exit(1) + } + + if err := (&infrav1alpha2.Metal3MachineList{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Metal3MachineList") + os.Exit(1) + } + + if err := (&infrav1alpha3.Metal3MachineList{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Metal3MachineList") + os.Exit(1) + } + + if err := (&infrav1alpha2.Metal3MachineTemplate{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Metal3MachineTemplate") + os.Exit(1) + } + + if err := (&infrav1alpha3.Metal3MachineTemplate{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Metal3MachineTemplate") + os.Exit(1) + } + if err := (&infrav1.Metal3MachineTemplate{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Metal3MachineTemplate") + os.Exit(1) + } + + if err := (&infrav1alpha2.Metal3MachineTemplateList{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Metal3MachineTemplateList") + os.Exit(1) + } + + if err := (&infrav1alpha3.Metal3MachineTemplateList{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Metal3MachineTemplateList") + os.Exit(1) + } + + if err := (&infrav1.Metal3DataTemplate{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Metal3DataTemplate") + os.Exit(1) + } + + if err := (&infrav1.Metal3Data{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Metal3Data") + os.Exit(1) + } + + if err := (&infrav1.Metal3DataClaim{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Metal3DataClaim") + os.Exit(1) + } + + if err := (&infrav1.Metal3IPPool{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Metal3IPPool") + os.Exit(1) + } + + if err := (&infrav1.Metal3IPAddress{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Metal3IPAddress") + os.Exit(1) + } + + if err := (&infrav1.Metal3IPClaim{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Metal3IPClaim") + os.Exit(1) + } +} From 52c2b53df4087265c118a36d1335d702c85ab516 Mon Sep 17 00:00:00 2001 From: maelk Date: Mon, 11 May 2020 19:01:40 +0300 Subject: [PATCH 2/9] Rename objects to strip Metal3 - Metal3IPPool -> IPPool - Metal3IPAddress -> IPAddress - Metal3IPClaim -> IPClaim --- Makefile | 78 +- README.md | 55 +- api/v1alpha1/conversion.go | 6 +- api/v1alpha1/ipaddress_types.go | 30 +- api/v1alpha1/ipaddress_webhook.go | 40 +- api/v1alpha1/ipaddress_webhook_test.go | 60 +- api/v1alpha1/ipclaim_types.go | 36 +- api/v1alpha1/ipclaim_webhook.go | 32 +- api/v1alpha1/ipclaim_webhook_test.go | 48 +- api/v1alpha1/ippool_types.go | 38 +- api/v1alpha1/ippool_webhook.go | 24 +- api/v1alpha1/ippool_webhook_test.go | 40 +- api/v1alpha1/zz_generated.deepcopy.go | 178 ++-- ...s.yaml => ipam.metal3.io_ipaddresses.yaml} | 27 +- ...aims.yaml => ipam.metal3.io_ipclaims.yaml} | 24 +- ...pools.yaml => ipam.metal3.io_ippools.yaml} | 18 +- config/crd/kustomization.yaml | 36 +- ...s.yaml => cainjection_in_ipaddresses.yaml} | 2 +- ...ools.yaml => cainjection_in_ipclaims.yaml} | 2 +- ...laims.yaml => cainjection_in_ippools.yaml} | 2 +- .../cainjection_in_metal3clusters.yaml | 8 - .../cainjection_in_metal3dataclaims.yaml | 8 - .../cainjection_in_metal3datatemplates.yaml | 8 - .../cainjection_in_metal3ipaddresses.yaml | 8 - .../cainjection_in_metal3machines.yaml | 8 - ...cainjection_in_metal3machinetemplates.yaml | 8 - ...datas.yaml => webhook_in_ipaddresses.yaml} | 2 +- ...3ippools.yaml => webhook_in_ipclaims.yaml} | 2 +- ...3clusters.yaml => webhook_in_ippools.yaml} | 2 +- .../patches/webhook_in_metal3dataclaims.yaml | 19 - .../webhook_in_metal3datatemplates.yaml | 19 - .../patches/webhook_in_metal3ipaddresses.yaml | 19 - .../patches/webhook_in_metal3ipclaims.yaml | 19 - .../patches/webhook_in_metal3machines.yaml | 19 - .../webhook_in_metal3machinetemplates.yaml | 19 - config/default/kustomization.yaml | 3 +- config/kustomization.yaml | 44 +- config/rbac/role.yaml | 162 +-- config/webhook/manifests.yaml | 48 +- ...ool_controller.go => ippool_controller.go} | 50 +- ...ller_test.go => ippool_controller_test.go} | 70 +- controllers/suite_test.go | 22 - docs/api.md | 951 ++---------------- docs/architecture.md | 459 --------- docs/deployment_workflow.md | 91 +- docs/dev-setup.md | 75 -- docs/getting-started.md | 219 ---- docs/images/components.png | Bin 90895 -> 0 bytes docs/images/controllerssequencediagram.png | Bin 55662 -> 0 bytes docs/images/fields_mapping.png | Bin 48250 -> 0 bytes docs/releasing.md | 25 +- examples/addons.yaml | 783 -------------- examples/cluster/cluster.yaml | 29 - examples/cluster/kustomizeconfig.yaml | 6 - .../clusterctl-cluster.yaml | 128 --- .../clusterctl-templates/example_variables.rc | 105 -- examples/controlplane/controlplane.yaml | 180 ---- examples/controlplane/kustomization.yaml | 7 - examples/controlplane/kustomizeconfig.yaml | 15 - examples/generate.sh | 46 +- examples/ippool/ippool.yaml | 16 + .../{cluster => ippool}/kustomization.yaml | 2 +- examples/ippool/kustomizeconfig.yaml | 6 + examples/machinedeployment/kustomization.yaml | 7 - .../machinedeployment/kustomizeconfig.yaml | 11 - .../machinedeployment/machinedeployment.yaml | 144 --- examples/metal3crds/kustomization.yaml | 5 - .../metal3crds/metal3.io_baremetalhosts.yaml | 574 ----------- examples/metal3plane/hosts.yaml | 749 -------------- examples/metal3plane/kustomization.yaml | 5 - .../provider-components/kustomization.yaml | 3 - .../manager_tolerations_patch.yaml | 44 +- ...al3ippool_manager.go => ippool_manager.go} | 66 +- ...manager_test.go => ippool_manager_test.go} | 302 +++--- ipam/manager_factory.go | 4 +- ipam/manager_factory_test.go | 2 +- ...ager.go => zz_generated.ippool_manager.go} | 2 +- ipam/mocks/zz_generated.manager_factory.go | 2 +- ipam/remote/remote.go | 46 - ipam/remote/remote_test.go | 118 --- ipam/suite_test.go | 13 - ipam/utils.go | 6 - ipam/utils_test.go | 46 +- main.go | 208 +--- 84 files changed, 796 insertions(+), 6047 deletions(-) rename config/crd/bases/{ipam.metal3.io_metal3ipaddresses.yaml => ipam.metal3.io_ipaddresses.yaml} (92%) rename config/crd/bases/{ipam.metal3.io_metal3ipclaims.yaml => ipam.metal3.io_ipclaims.yaml} (91%) rename config/crd/bases/{ipam.metal3.io_metal3ippools.yaml => ipam.metal3.io_ippools.yaml} (95%) rename config/crd/patches/{cainjection_in_metal3datas.yaml => cainjection_in_ipaddresses.yaml} (85%) rename config/crd/patches/{cainjection_in_metal3ippools.yaml => cainjection_in_ipclaims.yaml} (84%) rename config/crd/patches/{cainjection_in_metal3ipclaims.yaml => cainjection_in_ippools.yaml} (84%) delete mode 100644 config/crd/patches/cainjection_in_metal3clusters.yaml delete mode 100644 config/crd/patches/cainjection_in_metal3dataclaims.yaml delete mode 100644 config/crd/patches/cainjection_in_metal3datatemplates.yaml delete mode 100644 config/crd/patches/cainjection_in_metal3ipaddresses.yaml delete mode 100644 config/crd/patches/cainjection_in_metal3machines.yaml delete mode 100644 config/crd/patches/cainjection_in_metal3machinetemplates.yaml rename config/crd/patches/{webhook_in_metal3datas.yaml => webhook_in_ipaddresses.yaml} (92%) rename config/crd/patches/{webhook_in_metal3ippools.yaml => webhook_in_ipclaims.yaml} (92%) rename config/crd/patches/{webhook_in_metal3clusters.yaml => webhook_in_ippools.yaml} (92%) delete mode 100644 config/crd/patches/webhook_in_metal3dataclaims.yaml delete mode 100644 config/crd/patches/webhook_in_metal3datatemplates.yaml delete mode 100644 config/crd/patches/webhook_in_metal3ipaddresses.yaml delete mode 100644 config/crd/patches/webhook_in_metal3ipclaims.yaml delete mode 100644 config/crd/patches/webhook_in_metal3machines.yaml delete mode 100644 config/crd/patches/webhook_in_metal3machinetemplates.yaml rename controllers/{metal3ippool_controller.go => ippool_controller.go} (75%) rename controllers/{metal3ippool_controller_test.go => ippool_controller_test.go} (86%) delete mode 100644 docs/architecture.md delete mode 100644 docs/dev-setup.md delete mode 100644 docs/getting-started.md delete mode 100644 docs/images/components.png delete mode 100644 docs/images/controllerssequencediagram.png delete mode 100644 docs/images/fields_mapping.png delete mode 100644 examples/addons.yaml delete mode 100644 examples/cluster/cluster.yaml delete mode 100644 examples/cluster/kustomizeconfig.yaml delete mode 100644 examples/clusterctl-templates/clusterctl-cluster.yaml delete mode 100644 examples/clusterctl-templates/example_variables.rc delete mode 100644 examples/controlplane/controlplane.yaml delete mode 100644 examples/controlplane/kustomization.yaml delete mode 100644 examples/controlplane/kustomizeconfig.yaml create mode 100644 examples/ippool/ippool.yaml rename examples/{cluster => ippool}/kustomization.yaml (89%) create mode 100644 examples/ippool/kustomizeconfig.yaml delete mode 100644 examples/machinedeployment/kustomization.yaml delete mode 100644 examples/machinedeployment/kustomizeconfig.yaml delete mode 100644 examples/machinedeployment/machinedeployment.yaml delete mode 100644 examples/metal3crds/kustomization.yaml delete mode 100644 examples/metal3crds/metal3.io_baremetalhosts.yaml delete mode 100644 examples/metal3plane/hosts.yaml delete mode 100644 examples/metal3plane/kustomization.yaml rename ipam/{metal3ippool_manager.go => ippool_manager.go} (88%) rename ipam/{metal3ippool_manager_test.go => ippool_manager_test.go} (84%) rename ipam/mocks/{zz_generated.metal3ippool_manager.go => zz_generated.ippool_manager.go} (98%) delete mode 100644 ipam/remote/remote.go delete mode 100644 ipam/remote/remote_test.go diff --git a/Makefile b/Makefile index 7279b847..dcad1780 100644 --- a/Makefile +++ b/Makefile @@ -175,11 +175,11 @@ generate-go: $(CONTROLLER_GEN) $(MOCKGEN) $(CONVERSION_GEN) $(KUBEBUILDER) $(KUS object:headerFile=./hack/boilerplate/boilerplate.generatego.txt $(MOCKGEN) \ - -destination=./ipam/mocks/zz_generated.metal3ippool_manager.go \ - -source=./ipam/metal3ippool_manager.go \ + -destination=./ipam/mocks/zz_generated.ippool_manager.go \ + -source=./ipam/ippool_manager.go \ -package=ipam_mocks \ -copyright_file=./hack/boilerplate/boilerplate.generatego.txt \ - IPPoolManagerInterface + PoolManagerInterface $(MOCKGEN) \ -destination=./ipam/mocks/zz_generated.manager_factory.go \ @@ -273,14 +273,10 @@ deploy: generate-examples kubectl apply -f examples/_out/provider-components.yaml deploy-examples: - kubectl apply -f ./examples/_out/cluster.yaml - kubectl apply -f ./examples/_out/machinedeployment.yaml - kubectl apply -f ./examples/_out/controlplane.yaml + kubectl apply -f ./examples/_out/ippool.yaml delete-examples: - kubectl delete -f ./examples/_out/controlplane.yaml - kubectl delete -f ./examples/_out/machinedeployment.yaml - kubectl delete -f ./examples/_out/cluster.yaml + kubectl delete -f ./examples/_out/ippool.yaml ## -------------------------------------- @@ -310,9 +306,6 @@ release: clean-release ## Builds and push container images using the latest git .PHONY: release-manifests release-manifests: $(RELEASE_DIR) ## Builds the manifests to publish with a release kustomize build config > $(RELEASE_DIR)/infrastructure-components.yaml - cp metadata.yaml $(RELEASE_DIR)/metadata.yaml - cp examples/clusterctl-templates/clusterctl-cluster.yaml $(RELEASE_DIR)/cluster-template.yaml - cp examples/clusterctl-templates/example_variables.rc $(RELEASE_DIR)/example_variables.rc .PHONY: release-binaries release-binaries: ## Builds the binaries to publish with a release @@ -344,67 +337,6 @@ release-tag-latest: ## Adds the latest tag to the last build tag. release-notes: $(RELEASE_NOTES) ## Generates a release notes template to be used with a release. $(RELEASE_NOTES) -## -------------------------------------- -## Development -## -------------------------------------- - -.PHONY: create-cluster -create-cluster: $(CLUSTERCTL) ## Create a development Kubernetes cluster using examples - $(CLUSTERCTL) \ - create cluster -v 4 \ - --bootstrap-flags="name=clusterapi" \ - --bootstrap-type kind \ - -m ./examples/_out/controlplane.yaml \ - -c ./examples/_out/cluster.yaml \ - -p ./examples/_out/provider-components.yaml \ - -a ./examples/addons.yaml - - -.PHONY: create-cluster-management -create-cluster-management: $(CLUSTERCTL) ## Create a development Kubernetes cluster in a KIND management cluster. - kind create cluster --name=clusterapi - # Apply provider-components. - kubectl \ - --kubeconfig=$$(kind get kubeconfig-path --name="clusterapi") \ - create -f examples/_out/provider-components.yaml - # Create Cluster. - kubectl \ - --kubeconfig=$$(kind get kubeconfig-path --name="clusterapi") \ - create -f examples/_out/cluster.yaml - # Create control plane machine. - kubectl \ - --kubeconfig=$$(kind get kubeconfig-path --name="clusterapi") \ - create -f examples/_out/controlplane.yaml - # Get KubeConfig using clusterctl. - $(CLUSTERCTL) \ - alpha phases get-kubeconfig -v=3 \ - --kubeconfig=$$(kind get kubeconfig-path --name="clusterapi") \ - --namespace=default \ - --cluster-name=test1 - # Apply addons on the target cluster, waiting for the control-plane to become available. - $(CLUSTERCTL) \ - alpha phases apply-addons -v=3 \ - --kubeconfig=./kubeconfig \ - -a examples/addons.yaml - # Create a worker node with MachineDeployment. - kubectl \ - --kubeconfig=$$(kind get kubeconfig-path --name="clusterapi") \ - create -f examples/_out/machinedeployment.yaml - -.PHONY: delete-cluster -delete-cluster: $(CLUSTERCTL) ## Deletes the development Kubernetes Cluster "test1" - $(CLUSTERCTL) \ - delete cluster -v 4 \ - --bootstrap-type kind \ - --bootstrap-flags="name=clusterapi" \ - --cluster test1 \ - --kubeconfig ./kubeconfig \ - -p ./examples/_out/provider-components.yaml \ - -.PHONY: kind-reset -kind-reset: ## Destroys the "clusterapi" kind cluster. - kind delete cluster --name=clusterapi || true - ## -------------------------------------- ## Cleanup / Verification ## -------------------------------------- diff --git a/README.md b/README.md index ad111397..109eaf2b 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,39 @@ -# Cluster API Provider for Managed Bare Metal Hardware +# Metal3 IP Address Manager for Cluster API Provider Metal3 -[![Ubuntu V1alpha3 build status](https://jenkins.nordix.org/view/Airship/job/airship_master_v1a3_integration_test_ubuntu/badge/icon?subject=Ubuntu%20E2E%20V1alpha3)](https://jenkins.nordix.org/view/Airship/job/airship_master_v1a3_integration_test_ubuntu) -[![CentOS V1alpha3 build status](https://jenkins.nordix.org/view/Airship/job/airship_master_v1a3_integration_test_centos/badge/icon?subject=CentOS%20E2E%20V1alpha3)](https://jenkins.nordix.org/view/Airship/job/airship_master_v1a3_integration_test_centos) +[![Ubuntu V1alpha4 build status](https://jenkins.nordix.org/view/Airship/job/airship_master_v1a4_integration_test_ubuntu/badge/icon?subject=Ubuntu%20E2E%20V1alpha4)](https://jenkins.nordix.org/view/Airship/job/airship_master_v1a4_integration_test_ubuntu) +[![CentOS V1alpha4 build status](https://jenkins.nordix.org/view/Airship/job/airship_master_v1a4_integration_test_centos/badge/icon?subject=CentOS%20E2E%20V1alpha4)](https://jenkins.nordix.org/view/Airship/job/airship_master_v1a4_integration_test_centos) -This repository contains a Machine actuator implementation for the -Kubernetes [Cluster API](https://github.com/kubernetes-sigs/cluster-api/). +This repository contains a controller to manage static IP address allocations +in [Cluster API Provider Metal3](https://github.com/metal3-io/cluster-api-provider-metal3/). -For more information about this actuator and related repositories, see +For more information about this controller and related repositories, see [metal3.io](http://metal3.io/). ## Compatibility with Cluster API -| CAPM3 version | Cluster API version | Release | -|---------------|---------------------|---------| -| v1alpha2 | v1alpha2 | v0.2.X | -| v1alpha3 | v1alpha3 | v0.3.X | - -You can find information on how to use this provider with Cluster API and -clusterctl in the [getting-started](docs/getting-started.md). +| IPAM version | CAPM3 version | Cluster API version | +|-------------------|-------------------|---------------------| +| v1alpha1 (v0.1.X) | v1alpha4 (v0.4.X) | v1alpha3 (v0.3.X) | ## Development Environment -* See [metal3-dev-env](https://github.com/metal3-io/metal3-dev-env) for an - end-to-end development and test environment for - `cluster-api-provider-metal3` and - [baremetal-operator](https://github.com/metal3-io/baremetal-operator). -* [Setting up for tests](docs/dev-setup.md) +See [metal3-dev-env](https://github.com/metal3-io/metal3-dev-env) for an +end-to-end development and test environment for +[cluster-api-provider-metal3](https://github.com/metal3-io/cluster-api-provider-metal3/) +and [baremetal-operator](https://github.com/metal3-io/baremetal-operator). ## API See the [API Documentation](docs/api.md) for details about the objects used with -this `cluster-api` provider. You can also see the [cluster deployment +this controller. You can also see the [cluster deployment workflow](docs/deployment_workflow.md) for the outline of the deployment process. -## Architecture - -The architecture with the components involved is documented [here](docs/architecture.md) - ## Deployment and examples -### Deploy Bare Metal Operator CRDs and CRs - -for testing purposes only, when Bare Metal Operator is not deployed - -```sh - make deploy-bmo-cr -``` - -### Deploy CAPM3 +### Deploy IPAM -Deploys CAPM3 CRDs and deploys CAPI, CABPK, CACPK and CAPM3 controllers +Deploys IPAM CRDs and deploys IPAM controllers ```sh make deploy @@ -61,18 +44,18 @@ Deploys CAPM3 CRDs and deploys CAPI, CABPK, CACPK and CAPM3 controllers Runs CAPM3 controller locally ```sh - kubectl scale -n capm3-system deployment.v1.apps/capm3-controller-manager \ + kubectl scale -n capm3-system deployment.v1.apps/metal3-ipam-controller-manager \ --replicas 0 make run ``` -### Deploy an example cluster +### Deploy an example pool ```sh make deploy-examples ``` -### Delete the example cluster +### Delete the example pool ```sh make delete-examples diff --git a/api/v1alpha1/conversion.go b/api/v1alpha1/conversion.go index 5f383c17..0d67430d 100644 --- a/api/v1alpha1/conversion.go +++ b/api/v1alpha1/conversion.go @@ -16,6 +16,6 @@ limitations under the License. package v1alpha1 -func (*Metal3IPPool) Hub() {} -func (*Metal3IPAddress) Hub() {} -func (*Metal3IPClaim) Hub() {} +func (*IPPool) Hub() {} +func (*IPAddress) Hub() {} +func (*IPClaim) Hub() {} diff --git a/api/v1alpha1/ipaddress_types.go b/api/v1alpha1/ipaddress_types.go index f8118eba..ffb1a211 100644 --- a/api/v1alpha1/ipaddress_types.go +++ b/api/v1alpha1/ipaddress_types.go @@ -22,18 +22,18 @@ import ( ) const ( - // DataFinalizer allows Metal3IPAddressReconciler to clean up resources - // associated with Metal3IPAddress before removing it from the apiserver. - IPAddressFinalizer = "metal3ipaddress.infrastructure.cluster.x-k8s.io" + // DataFinalizer allows IPAddressReconciler to clean up resources + // associated with IPAddress before removing it from the apiserver. + IPAddressFinalizer = "ipaddress.ipam.metal3.io" ) -// Metal3IPAddressSpec defines the desired state of Metal3IPAddress. -type Metal3IPAddressSpec struct { +// IPAddressSpec defines the desired state of IPAddress. +type IPAddressSpec struct { - // Claim points to the object the Metal3IPClaim was created for. + // Claim points to the object the IPClaim was created for. Claim corev1.ObjectReference `json:"claim"` - // Pool is the Metal3IPPool this was generated from. + // Pool is the IPPool this was generated from. Pool corev1.ObjectReference `json:"pool"` // +kubebuilder:validation:Maximum=128 @@ -48,26 +48,26 @@ type Metal3IPAddressSpec struct { } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -// +kubebuilder:resource:path=metal3ipaddresses,scope=Namespaced,categories=cluster-api,shortName=m3ipa;m3ipaddress;metal3ipaddress +// +kubebuilder:resource:path=ipaddresses,scope=Namespaced,categories=metal3,shortName=ipa;ipaddress;ipaddress // +kubebuilder:storageversion // +kubebuilder:object:root=true -// Metal3IPAddress is the Schema for the metal3ipaddresses API -type Metal3IPAddress struct { +// IPAddress is the Schema for the ipaddresses API +type IPAddress struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec Metal3IPAddressSpec `json:"spec,omitempty"` + Spec IPAddressSpec `json:"spec,omitempty"` } // +kubebuilder:object:root=true -// Metal3IPAddressList contains a list of Metal3IPAddress -type Metal3IPAddressList struct { +// IPAddressList contains a list of IPAddress +type IPAddressList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` - Items []Metal3IPAddress `json:"items"` + Items []IPAddress `json:"items"` } func init() { - SchemeBuilder.Register(&Metal3IPAddress{}, &Metal3IPAddressList{}) + SchemeBuilder.Register(&IPAddress{}, &IPAddressList{}) } diff --git a/api/v1alpha1/ipaddress_webhook.go b/api/v1alpha1/ipaddress_webhook.go index ec284de2..b413621f 100644 --- a/api/v1alpha1/ipaddress_webhook.go +++ b/api/v1alpha1/ipaddress_webhook.go @@ -22,23 +22,23 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" ) -func (c *Metal3IPAddress) SetupWebhookWithManager(mgr ctrl.Manager) error { +func (c *IPAddress) SetupWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr). For(c). Complete() } -// +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1alpha4-metal3ipaddress,mutating=false,failurePolicy=fail,groups=infrastructure.cluster.x-k8s.io,resources=metal3ipaddresses,versions=v1alpha4,name=validation.metal3ipaddress.infrastructure.cluster.x-k8s.io,matchPolicy=Equivalent -// +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1alpha4-metal3ipaddress,mutating=true,failurePolicy=fail,groups=infrastructure.cluster.x-k8s.io,resources=metal3ipaddresses,versions=v1alpha4,name=default.metal3ipaddress.infrastructure.cluster.x-k8s.io,matchPolicy=Equivalent +// +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1alpha4-ipaddress,mutating=false,failurePolicy=fail,groups=ipam.metal3.io,resources=ipaddresses,versions=v1alpha4,name=validation.ipaddress.ipam.metal3.io,matchPolicy=Equivalent +// +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1alpha4-ipaddress,mutating=true,failurePolicy=fail,groups=ipam.metal3.io,resources=ipaddresses,versions=v1alpha4,name=default.ipaddress.ipam.metal3.io,matchPolicy=Equivalent -var _ webhook.Defaulter = &Metal3IPAddress{} -var _ webhook.Validator = &Metal3IPAddress{} +var _ webhook.Defaulter = &IPAddress{} +var _ webhook.Validator = &IPAddress{} -func (c *Metal3IPAddress) Default() { +func (c *IPAddress) Default() { } // ValidateCreate implements webhook.Validator so a webhook will be registered for the type -func (c *Metal3IPAddress) ValidateCreate() error { +func (c *IPAddress) ValidateCreate() error { allErrs := field.ErrorList{} if c.Spec.Pool.Name == "" { allErrs = append(allErrs, @@ -73,18 +73,18 @@ func (c *Metal3IPAddress) ValidateCreate() error { if len(allErrs) == 0 { return nil } - return apierrors.NewInvalid(GroupVersion.WithKind("Metal3IPAddress").GroupKind(), c.Name, allErrs) + return apierrors.NewInvalid(GroupVersion.WithKind("IPAddress").GroupKind(), c.Name, allErrs) } // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type -func (c *Metal3IPAddress) ValidateUpdate(old runtime.Object) error { +func (c *IPAddress) ValidateUpdate(old runtime.Object) error { allErrs := field.ErrorList{} - oldMetal3IPAddress, ok := old.(*Metal3IPAddress) - if !ok || oldMetal3IPAddress == nil { + oldIPAddress, ok := old.(*IPAddress) + if !ok || oldIPAddress == nil { return apierrors.NewInternalError(errors.New("unable to convert existing object")) } - if c.Spec.Address != oldMetal3IPAddress.Spec.Address { + if c.Spec.Address != oldIPAddress.Spec.Address { allErrs = append(allErrs, field.Invalid( field.NewPath("spec", "address"), @@ -94,7 +94,7 @@ func (c *Metal3IPAddress) ValidateUpdate(old runtime.Object) error { ) } - if c.Spec.Pool.Name != oldMetal3IPAddress.Spec.Pool.Name { + if c.Spec.Pool.Name != oldIPAddress.Spec.Pool.Name { allErrs = append(allErrs, field.Invalid( field.NewPath("spec", "pool"), @@ -102,7 +102,7 @@ func (c *Metal3IPAddress) ValidateUpdate(old runtime.Object) error { "cannot be modified", ), ) - } else if c.Spec.Pool.Namespace != oldMetal3IPAddress.Spec.Pool.Namespace { + } else if c.Spec.Pool.Namespace != oldIPAddress.Spec.Pool.Namespace { allErrs = append(allErrs, field.Invalid( field.NewPath("spec", "pool"), @@ -110,7 +110,7 @@ func (c *Metal3IPAddress) ValidateUpdate(old runtime.Object) error { "cannot be modified", ), ) - } else if c.Spec.Pool.Kind != oldMetal3IPAddress.Spec.Pool.Kind { + } else if c.Spec.Pool.Kind != oldIPAddress.Spec.Pool.Kind { allErrs = append(allErrs, field.Invalid( field.NewPath("spec", "pool"), @@ -120,7 +120,7 @@ func (c *Metal3IPAddress) ValidateUpdate(old runtime.Object) error { ) } - if c.Spec.Claim.Name != oldMetal3IPAddress.Spec.Claim.Name { + if c.Spec.Claim.Name != oldIPAddress.Spec.Claim.Name { allErrs = append(allErrs, field.Invalid( field.NewPath("spec", "claim"), @@ -128,7 +128,7 @@ func (c *Metal3IPAddress) ValidateUpdate(old runtime.Object) error { "cannot be modified", ), ) - } else if c.Spec.Claim.Namespace != oldMetal3IPAddress.Spec.Claim.Namespace { + } else if c.Spec.Claim.Namespace != oldIPAddress.Spec.Claim.Namespace { allErrs = append(allErrs, field.Invalid( field.NewPath("spec", "claim"), @@ -136,7 +136,7 @@ func (c *Metal3IPAddress) ValidateUpdate(old runtime.Object) error { "cannot be modified", ), ) - } else if c.Spec.Claim.Kind != oldMetal3IPAddress.Spec.Claim.Kind { + } else if c.Spec.Claim.Kind != oldIPAddress.Spec.Claim.Kind { allErrs = append(allErrs, field.Invalid( field.NewPath("spec", "claim"), @@ -149,10 +149,10 @@ func (c *Metal3IPAddress) ValidateUpdate(old runtime.Object) error { if len(allErrs) == 0 { return nil } - return apierrors.NewInvalid(GroupVersion.WithKind("Metal3IPAddress").GroupKind(), c.Name, allErrs) + return apierrors.NewInvalid(GroupVersion.WithKind("IPAddress").GroupKind(), c.Name, allErrs) } // ValidateDelete implements webhook.Validator so a webhook will be registered for the type -func (c *Metal3IPAddress) ValidateDelete() error { +func (c *IPAddress) ValidateDelete() error { return nil } diff --git a/api/v1alpha1/ipaddress_webhook_test.go b/api/v1alpha1/ipaddress_webhook_test.go index fc4ac7bf..1078035c 100644 --- a/api/v1alpha1/ipaddress_webhook_test.go +++ b/api/v1alpha1/ipaddress_webhook_test.go @@ -21,21 +21,21 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func TestMetal3IPAddressDefault(t *testing.T) { +func TestIPAddressDefault(t *testing.T) { g := NewWithT(t) - c := &Metal3IPAddress{ + c := &IPAddress{ ObjectMeta: metav1.ObjectMeta{ Namespace: "foo", }, - Spec: Metal3IPAddressSpec{}, + Spec: IPAddressSpec{}, } c.Default() - g.Expect(c.Spec).To(Equal(Metal3IPAddressSpec{})) + g.Expect(c.Spec).To(Equal(IPAddressSpec{})) } -func TestMetal3IPAddressCreateValidation(t *testing.T) { +func TestIPAddressCreateValidation(t *testing.T) { tests := []struct { name string @@ -98,12 +98,12 @@ func TestMetal3IPAddressCreateValidation(t *testing.T) { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) - obj := &Metal3IPAddress{ + obj := &IPAddress{ ObjectMeta: metav1.ObjectMeta{ Namespace: "foo", Name: tt.addressName, }, - Spec: Metal3IPAddressSpec{ + Spec: IPAddressSpec{ Pool: tt.ipPool, Address: tt.address, Claim: tt.claim, @@ -121,18 +121,18 @@ func TestMetal3IPAddressCreateValidation(t *testing.T) { } } -func TestMetal3IPAddressUpdateValidation(t *testing.T) { +func TestIPAddressUpdateValidation(t *testing.T) { tests := []struct { name string expectErr bool - new *Metal3IPAddressSpec - old *Metal3IPAddressSpec + new *IPAddressSpec + old *IPAddressSpec }{ { name: "should succeed when values are the same", expectErr: false, - new: &Metal3IPAddressSpec{ + new: &IPAddressSpec{ Pool: corev1.ObjectReference{ Name: "abc", }, @@ -141,7 +141,7 @@ func TestMetal3IPAddressUpdateValidation(t *testing.T) { }, Address: "abcd", }, - old: &Metal3IPAddressSpec{ + old: &IPAddressSpec{ Pool: corev1.ObjectReference{ Name: "abc", }, @@ -154,7 +154,7 @@ func TestMetal3IPAddressUpdateValidation(t *testing.T) { { name: "should fail with nil old", expectErr: true, - new: &Metal3IPAddressSpec{ + new: &IPAddressSpec{ Pool: corev1.ObjectReference{ Name: "abc", }, @@ -165,13 +165,13 @@ func TestMetal3IPAddressUpdateValidation(t *testing.T) { { name: "should fail when index changes", expectErr: true, - new: &Metal3IPAddressSpec{ + new: &IPAddressSpec{ Pool: corev1.ObjectReference{ Name: "abc", }, Address: "abcd", }, - old: &Metal3IPAddressSpec{ + old: &IPAddressSpec{ Pool: corev1.ObjectReference{ Name: "abc", }, @@ -181,13 +181,13 @@ func TestMetal3IPAddressUpdateValidation(t *testing.T) { { name: "should fail when pool name changes", expectErr: true, - new: &Metal3IPAddressSpec{ + new: &IPAddressSpec{ Pool: corev1.ObjectReference{ Name: "abc", }, Address: "abcd", }, - old: &Metal3IPAddressSpec{ + old: &IPAddressSpec{ Pool: corev1.ObjectReference{ Name: "abcd", }, @@ -197,14 +197,14 @@ func TestMetal3IPAddressUpdateValidation(t *testing.T) { { name: "should fail when Pool Namespace changes", expectErr: true, - new: &Metal3IPAddressSpec{ + new: &IPAddressSpec{ Pool: corev1.ObjectReference{ Name: "abc", Namespace: "abc", }, Address: "abcd", }, - old: &Metal3IPAddressSpec{ + old: &IPAddressSpec{ Pool: corev1.ObjectReference{ Name: "abc", Namespace: "abcd", @@ -215,14 +215,14 @@ func TestMetal3IPAddressUpdateValidation(t *testing.T) { { name: "should fail when Pool kind changes", expectErr: true, - new: &Metal3IPAddressSpec{ + new: &IPAddressSpec{ Pool: corev1.ObjectReference{ Name: "abc", Kind: "abc", }, Address: "abcd", }, - old: &Metal3IPAddressSpec{ + old: &IPAddressSpec{ Pool: corev1.ObjectReference{ Name: "abc", Kind: "abcd", @@ -233,13 +233,13 @@ func TestMetal3IPAddressUpdateValidation(t *testing.T) { { name: "should fail when Claim name changes", expectErr: true, - new: &Metal3IPAddressSpec{ + new: &IPAddressSpec{ Claim: corev1.ObjectReference{ Name: "abc", }, Address: "abcd", }, - old: &Metal3IPAddressSpec{ + old: &IPAddressSpec{ Claim: corev1.ObjectReference{ Name: "abcd", }, @@ -249,14 +249,14 @@ func TestMetal3IPAddressUpdateValidation(t *testing.T) { { name: "should fail when Claim Namespace changes", expectErr: true, - new: &Metal3IPAddressSpec{ + new: &IPAddressSpec{ Claim: corev1.ObjectReference{ Name: "abc", Namespace: "abc", }, Address: "abcd", }, - old: &Metal3IPAddressSpec{ + old: &IPAddressSpec{ Claim: corev1.ObjectReference{ Name: "abc", Namespace: "abcd", @@ -267,14 +267,14 @@ func TestMetal3IPAddressUpdateValidation(t *testing.T) { { name: "should fail when Claim kind changes", expectErr: true, - new: &Metal3IPAddressSpec{ + new: &IPAddressSpec{ Claim: corev1.ObjectReference{ Name: "abc", Kind: "abc", }, Address: "abcd", }, - old: &Metal3IPAddressSpec{ + old: &IPAddressSpec{ Claim: corev1.ObjectReference{ Name: "abc", Kind: "abcd", @@ -286,9 +286,9 @@ func TestMetal3IPAddressUpdateValidation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - var new, old *Metal3IPAddress + var new, old *IPAddress g := NewWithT(t) - new = &Metal3IPAddress{ + new = &IPAddress{ ObjectMeta: metav1.ObjectMeta{ Namespace: "foo", Name: "abc-1", @@ -297,7 +297,7 @@ func TestMetal3IPAddressUpdateValidation(t *testing.T) { } if tt.old != nil { - old = &Metal3IPAddress{ + old = &IPAddress{ ObjectMeta: metav1.ObjectMeta{ Namespace: "foo", Name: "abc-1", diff --git a/api/v1alpha1/ipclaim_types.go b/api/v1alpha1/ipclaim_types.go index 095ab814..8f48e796 100644 --- a/api/v1alpha1/ipclaim_types.go +++ b/api/v1alpha1/ipclaim_types.go @@ -22,22 +22,22 @@ import ( ) const ( - // IPClaimFinalizer allows Metal3IPClaimReconciler to clean up resources - // associated with Metal3IPClaim before removing it from the apiserver. - IPClaimFinalizer = "metal3ipclaim.infrastructure.cluster.x-k8s.io" + // IPClaimFinalizer allows IPClaimReconciler to clean up resources + // associated with IPClaim before removing it from the apiserver. + IPClaimFinalizer = "ipclaim.ipam.metal3.io" ) -// Metal3IPClaimSpec defines the desired state of Metal3IPClaim. -type Metal3IPClaimSpec struct { +// IPClaimSpec defines the desired state of IPClaim. +type IPClaimSpec struct { - // Pool is the Metal3IPPool this was generated from. + // Pool is the IPPool this was generated from. Pool corev1.ObjectReference `json:"pool"` } -// Metal3IPClaimStatus defines the observed state of Metal3IPClaim. -type Metal3IPClaimStatus struct { +// IPClaimStatus defines the observed state of IPClaim. +type IPClaimStatus struct { - // Address is the Metal3IPAddress that was generated for this claim. + // Address is the IPAddress that was generated for this claim. Address *corev1.ObjectReference `json:"address,omitempty"` // ErrorMessage contains the error message @@ -45,28 +45,28 @@ type Metal3IPClaimStatus struct { } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -// +kubebuilder:resource:path=metal3ipclaims,scope=Namespaced,categories=cluster-api,shortName=m3ipc;m3ipclaim;metal3ipclaim +// +kubebuilder:resource:path=ipclaims,scope=Namespaced,categories=cluster-api,shortName=m3ipc;m3ipclaim;ipclaim // +kubebuilder:storageversion // +kubebuilder:subresource:status // +kubebuilder:object:root=true -// Metal3IPClaim is the Schema for the metal3ipclaims API -type Metal3IPClaim struct { +// IPClaim is the Schema for the ipclaims API +type IPClaim struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec Metal3IPClaimSpec `json:"spec,omitempty"` - Status Metal3IPClaimStatus `json:"status,omitempty"` + Spec IPClaimSpec `json:"spec,omitempty"` + Status IPClaimStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true -// Metal3IPClaimList contains a list of Metal3IPClaim -type Metal3IPClaimList struct { +// IPClaimList contains a list of IPClaim +type IPClaimList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` - Items []Metal3IPClaim `json:"items"` + Items []IPClaim `json:"items"` } func init() { - SchemeBuilder.Register(&Metal3IPClaim{}, &Metal3IPClaimList{}) + SchemeBuilder.Register(&IPClaim{}, &IPClaimList{}) } diff --git a/api/v1alpha1/ipclaim_webhook.go b/api/v1alpha1/ipclaim_webhook.go index 3e1ca637..fd81eed9 100644 --- a/api/v1alpha1/ipclaim_webhook.go +++ b/api/v1alpha1/ipclaim_webhook.go @@ -22,23 +22,23 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" ) -func (c *Metal3IPClaim) SetupWebhookWithManager(mgr ctrl.Manager) error { +func (c *IPClaim) SetupWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr). For(c). Complete() } -// +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1alpha4-metal3ipclaim,mutating=false,failurePolicy=fail,groups=infrastructure.cluster.x-k8s.io,resources=metal3ipclaims,versions=v1alpha4,name=validation.metal3ipclaim.infrastructure.cluster.x-k8s.io,matchPolicy=Equivalent -// +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1alpha4-metal3ipclaim,mutating=true,failurePolicy=fail,groups=infrastructure.cluster.x-k8s.io,resources=metal3ipclaims,versions=v1alpha4,name=default.metal3ipclaim.infrastructure.cluster.x-k8s.io,matchPolicy=Equivalent +// +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1alpha4-ipclaim,mutating=false,failurePolicy=fail,groups=ipam.metal3.io,resources=ipclaims,versions=v1alpha4,name=validation.ipclaim.ipam.metal3.io,matchPolicy=Equivalent +// +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1alpha4-ipclaim,mutating=true,failurePolicy=fail,groups=ipam.metal3.io,resources=ipclaims,versions=v1alpha4,name=default.ipclaim.ipam.metal3.io,matchPolicy=Equivalent -var _ webhook.Defaulter = &Metal3IPClaim{} -var _ webhook.Validator = &Metal3IPClaim{} +var _ webhook.Defaulter = &IPClaim{} +var _ webhook.Validator = &IPClaim{} -func (c *Metal3IPClaim) Default() { +func (c *IPClaim) Default() { } // ValidateCreate implements webhook.Validator so a webhook will be registered for the type -func (c *Metal3IPClaim) ValidateCreate() error { +func (c *IPClaim) ValidateCreate() error { allErrs := field.ErrorList{} if c.Spec.Pool.Name == "" { allErrs = append(allErrs, @@ -53,18 +53,18 @@ func (c *Metal3IPClaim) ValidateCreate() error { if len(allErrs) == 0 { return nil } - return apierrors.NewInvalid(GroupVersion.WithKind("Metal3IPClaim").GroupKind(), c.Name, allErrs) + return apierrors.NewInvalid(GroupVersion.WithKind("IPClaim").GroupKind(), c.Name, allErrs) } // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type -func (c *Metal3IPClaim) ValidateUpdate(old runtime.Object) error { +func (c *IPClaim) ValidateUpdate(old runtime.Object) error { allErrs := field.ErrorList{} - oldMetal3IPClaim, ok := old.(*Metal3IPClaim) - if !ok || oldMetal3IPClaim == nil { + oldIPClaim, ok := old.(*IPClaim) + if !ok || oldIPClaim == nil { return apierrors.NewInternalError(errors.New("unable to convert existing object")) } - if c.Spec.Pool.Name != oldMetal3IPClaim.Spec.Pool.Name { + if c.Spec.Pool.Name != oldIPClaim.Spec.Pool.Name { allErrs = append(allErrs, field.Invalid( field.NewPath("spec", "pool"), @@ -72,7 +72,7 @@ func (c *Metal3IPClaim) ValidateUpdate(old runtime.Object) error { "cannot be modified", ), ) - } else if c.Spec.Pool.Namespace != oldMetal3IPClaim.Spec.Pool.Namespace { + } else if c.Spec.Pool.Namespace != oldIPClaim.Spec.Pool.Namespace { allErrs = append(allErrs, field.Invalid( field.NewPath("spec", "pool"), @@ -80,7 +80,7 @@ func (c *Metal3IPClaim) ValidateUpdate(old runtime.Object) error { "cannot be modified", ), ) - } else if c.Spec.Pool.Kind != oldMetal3IPClaim.Spec.Pool.Kind { + } else if c.Spec.Pool.Kind != oldIPClaim.Spec.Pool.Kind { allErrs = append(allErrs, field.Invalid( field.NewPath("spec", "pool"), @@ -93,10 +93,10 @@ func (c *Metal3IPClaim) ValidateUpdate(old runtime.Object) error { if len(allErrs) == 0 { return nil } - return apierrors.NewInvalid(GroupVersion.WithKind("Metal3IPClaim").GroupKind(), c.Name, allErrs) + return apierrors.NewInvalid(GroupVersion.WithKind("IPClaim").GroupKind(), c.Name, allErrs) } // ValidateDelete implements webhook.Validator so a webhook will be registered for the type -func (c *Metal3IPClaim) ValidateDelete() error { +func (c *IPClaim) ValidateDelete() error { return nil } diff --git a/api/v1alpha1/ipclaim_webhook_test.go b/api/v1alpha1/ipclaim_webhook_test.go index 45c474b3..a3cf1160 100644 --- a/api/v1alpha1/ipclaim_webhook_test.go +++ b/api/v1alpha1/ipclaim_webhook_test.go @@ -21,21 +21,21 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func TestMetal3IPClaimDefault(t *testing.T) { +func TestIPClaimDefault(t *testing.T) { g := NewWithT(t) - c := &Metal3IPClaim{ + c := &IPClaim{ ObjectMeta: metav1.ObjectMeta{ Namespace: "foo", }, } c.Default() - g.Expect(c.Spec).To(Equal(Metal3IPClaimSpec{})) - g.Expect(c.Status).To(Equal(Metal3IPClaimStatus{})) + g.Expect(c.Spec).To(Equal(IPClaimSpec{})) + g.Expect(c.Status).To(Equal(IPClaimStatus{})) } -func TestMetal3IPClaimCreateValidation(t *testing.T) { +func TestIPClaimCreateValidation(t *testing.T) { tests := []struct { name string @@ -71,12 +71,12 @@ func TestMetal3IPClaimCreateValidation(t *testing.T) { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) - obj := &Metal3IPClaim{ + obj := &IPClaim{ ObjectMeta: metav1.ObjectMeta{ Namespace: "foo", Name: tt.claimName, }, - Spec: Metal3IPClaimSpec{ + Spec: IPClaimSpec{ Pool: tt.ipPool, }, } @@ -92,23 +92,23 @@ func TestMetal3IPClaimCreateValidation(t *testing.T) { } } -func TestMetal3IPClaimUpdateValidation(t *testing.T) { +func TestIPClaimUpdateValidation(t *testing.T) { tests := []struct { name string expectErr bool - new *Metal3IPClaimSpec - old *Metal3IPClaimSpec + new *IPClaimSpec + old *IPClaimSpec }{ { name: "should succeed when values are the same", expectErr: false, - new: &Metal3IPClaimSpec{ + new: &IPClaimSpec{ Pool: corev1.ObjectReference{ Name: "abc", }, }, - old: &Metal3IPClaimSpec{ + old: &IPClaimSpec{ Pool: corev1.ObjectReference{ Name: "abc", }, @@ -117,7 +117,7 @@ func TestMetal3IPClaimUpdateValidation(t *testing.T) { { name: "should fail with nil old", expectErr: true, - new: &Metal3IPClaimSpec{ + new: &IPClaimSpec{ Pool: corev1.ObjectReference{ Name: "abc", }, @@ -127,10 +127,10 @@ func TestMetal3IPClaimUpdateValidation(t *testing.T) { { name: "should fail when pool is unset", expectErr: true, - new: &Metal3IPClaimSpec{ + new: &IPClaimSpec{ Pool: corev1.ObjectReference{}, }, - old: &Metal3IPClaimSpec{ + old: &IPClaimSpec{ Pool: corev1.ObjectReference{ Name: "abc", }, @@ -139,12 +139,12 @@ func TestMetal3IPClaimUpdateValidation(t *testing.T) { { name: "should fail when pool name changes", expectErr: true, - new: &Metal3IPClaimSpec{ + new: &IPClaimSpec{ Pool: corev1.ObjectReference{ Name: "abc", }, }, - old: &Metal3IPClaimSpec{ + old: &IPClaimSpec{ Pool: corev1.ObjectReference{ Name: "abcd", }, @@ -153,13 +153,13 @@ func TestMetal3IPClaimUpdateValidation(t *testing.T) { { name: "should fail when Pool Namespace changes", expectErr: true, - new: &Metal3IPClaimSpec{ + new: &IPClaimSpec{ Pool: corev1.ObjectReference{ Name: "abc", Namespace: "abc", }, }, - old: &Metal3IPClaimSpec{ + old: &IPClaimSpec{ Pool: corev1.ObjectReference{ Name: "abc", Namespace: "abcd", @@ -169,13 +169,13 @@ func TestMetal3IPClaimUpdateValidation(t *testing.T) { { name: "should fail when Pool kind changes", expectErr: true, - new: &Metal3IPClaimSpec{ + new: &IPClaimSpec{ Pool: corev1.ObjectReference{ Name: "abc", Kind: "abc", }, }, - old: &Metal3IPClaimSpec{ + old: &IPClaimSpec{ Pool: corev1.ObjectReference{ Name: "abc", Kind: "abcd", @@ -186,9 +186,9 @@ func TestMetal3IPClaimUpdateValidation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - var new, old *Metal3IPClaim + var new, old *IPClaim g := NewWithT(t) - new = &Metal3IPClaim{ + new = &IPClaim{ ObjectMeta: metav1.ObjectMeta{ Namespace: "foo", Name: "abc-1", @@ -197,7 +197,7 @@ func TestMetal3IPClaimUpdateValidation(t *testing.T) { } if tt.old != nil { - old = &Metal3IPClaim{ + old = &IPClaim{ ObjectMeta: metav1.ObjectMeta{ Namespace: "foo", Name: "abc-1", diff --git a/api/v1alpha1/ippool_types.go b/api/v1alpha1/ippool_types.go index e645b595..b27f3da4 100644 --- a/api/v1alpha1/ippool_types.go +++ b/api/v1alpha1/ippool_types.go @@ -21,14 +21,14 @@ import ( ) const ( - // IPPoolFinalizer allows Metal3IPPoolReconciler to clean up resources - // associated with Metal3IPPool before removing it from the apiserver. - IPPoolFinalizer = "metal3ippool.infrastructure.cluster.x-k8s.io" + // IPPoolFinalizer allows IPPoolReconciler to clean up resources + // associated with IPPool before removing it from the apiserver. + IPPoolFinalizer = "ippool.ipam.metal3.io" ) // MetaDataIPAddress contains the info to render th ip address. It is IP-version // agnostic -type IPPool struct { +type Pool struct { // Start is the first ip address that can be rendered Start *IPAddressStr `json:"start,omitempty"` @@ -50,15 +50,15 @@ type IPPool struct { Gateway *IPAddressStr `json:"gateway,omitempty"` } -// Metal3IPPoolSpec defines the desired state of Metal3IPPool. -type Metal3IPPoolSpec struct { +// IPPoolSpec defines the desired state of IPPool. +type IPPoolSpec struct { // ClusterName is the name of the Cluster this object belongs to. // +kubebuilder:validation:MinLength=1 ClusterName string `json:"clusterName"` //Pools contains the list of IP addresses pools - Pools []IPPool `json:"pools,omitempty"` + Pools []Pool `json:"pools,omitempty"` // PreAllocations contains the preallocated IP addresses PreAllocations map[string]IPAddressStr `json:"preAllocations,omitempty"` @@ -71,12 +71,12 @@ type Metal3IPPoolSpec struct { Gateway *IPAddressStr `json:"gateway,omitempty"` // +kubebuilder:validation:MinLength=1 - // namePrefix is the prefix used to generate the Metal3IPAddress object names + // namePrefix is the prefix used to generate the IPAddress object names NamePrefix string `json:"namePrefix"` } -// Metal3IPPoolStatus defines the observed state of Metal3IPPool. -type Metal3IPPoolStatus struct { +// IPPoolStatus defines the observed state of IPPool. +type IPPoolStatus struct { // LastUpdated identifies when this status was last observed. // +optional LastUpdated *metav1.Time `json:"lastUpdated,omitempty"` @@ -86,30 +86,30 @@ type Metal3IPPoolStatus struct { } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -// +kubebuilder:resource:path=metal3ippools,scope=Namespaced,categories=cluster-api,shortName=m3ipp;m3ippool +// +kubebuilder:resource:path=ippools,scope=Namespaced,categories=cluster-api,shortName=m3ipp;m3ippool // +kubebuilder:storageversion // +kubebuilder:subresource:status // +kubebuilder:object:root=true // +kubebuilder:printcolumn:name="Cluster",type="string",JSONPath=".metadata.labels.cluster\\.x-k8s\\.io/cluster-name",description="Cluster to which this template belongs" -// Metal3IPPool is the Schema for the metal3ippools API -type Metal3IPPool struct { +// IPPool is the Schema for the ippools API +type IPPool struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec Metal3IPPoolSpec `json:"spec,omitempty"` - Status Metal3IPPoolStatus `json:"status,omitempty"` + Spec IPPoolSpec `json:"spec,omitempty"` + Status IPPoolStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true -// Metal3IPPoolList contains a list of Metal3IPPool -type Metal3IPPoolList struct { +// IPPoolList contains a list of IPPool +type IPPoolList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` - Items []Metal3IPPool `json:"items"` + Items []IPPool `json:"items"` } func init() { - SchemeBuilder.Register(&Metal3IPPool{}, &Metal3IPPoolList{}) + SchemeBuilder.Register(&IPPool{}, &IPPoolList{}) } diff --git a/api/v1alpha1/ippool_webhook.go b/api/v1alpha1/ippool_webhook.go index d27a92ae..212981ee 100644 --- a/api/v1alpha1/ippool_webhook.go +++ b/api/v1alpha1/ippool_webhook.go @@ -24,30 +24,30 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" ) -func (c *Metal3IPPool) SetupWebhookWithManager(mgr ctrl.Manager) error { +func (c *IPPool) SetupWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr). For(c). Complete() } -// +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1alpha4-metal3ippool,mutating=false,failurePolicy=fail,groups=infrastructure.cluster.x-k8s.io,resources=metal3ippools,versions=v1alpha4,name=validation.metal3ippool.infrastructure.cluster.x-k8s.io,matchPolicy=Equivalent -// +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1alpha4-metal3ippool,mutating=true,failurePolicy=fail,groups=infrastructure.cluster.x-k8s.io,resources=metal3ippools,versions=v1alpha4,name=default.metal3ippool.infrastructure.cluster.x-k8s.io,matchPolicy=Equivalent +// +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1alpha4-ippool,mutating=false,failurePolicy=fail,groups=ipam.metal3.io,resources=ippools,versions=v1alpha4,name=validation.ippool.ipam.metal3.io,matchPolicy=Equivalent +// +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1alpha4-ippool,mutating=true,failurePolicy=fail,groups=ipam.metal3.io,resources=ippools,versions=v1alpha4,name=default.ippool.ipam.metal3.io,matchPolicy=Equivalent -var _ webhook.Defaulter = &Metal3IPPool{} -var _ webhook.Validator = &Metal3IPPool{} +var _ webhook.Defaulter = &IPPool{} +var _ webhook.Validator = &IPPool{} -func (c *Metal3IPPool) Default() { +func (c *IPPool) Default() { } // ValidateCreate implements webhook.Validator so a webhook will be registered for the type -func (c *Metal3IPPool) ValidateCreate() error { +func (c *IPPool) ValidateCreate() error { return c.validate() } // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type -func (c *Metal3IPPool) ValidateUpdate(old runtime.Object) error { +func (c *IPPool) ValidateUpdate(old runtime.Object) error { allErrs := field.ErrorList{} - oldM3ipp, ok := old.(*Metal3IPPool) + oldM3ipp, ok := old.(*IPPool) if !ok || oldM3ipp == nil { return apierrors.NewInternalError(errors.New("unable to convert existing object")) } @@ -69,16 +69,16 @@ func (c *Metal3IPPool) ValidateUpdate(old runtime.Object) error { } // ValidateDelete implements webhook.Validator so a webhook will be registered for the type -func (c *Metal3IPPool) ValidateDelete() error { +func (c *IPPool) ValidateDelete() error { return nil } //No further validation for now -func (c *Metal3IPPool) validate() error { +func (c *IPPool) validate() error { var allErrs field.ErrorList if len(allErrs) == 0 { return nil } - return apierrors.NewInvalid(GroupVersion.WithKind("Metal3IPPool").GroupKind(), c.Name, allErrs) + return apierrors.NewInvalid(GroupVersion.WithKind("IPPool").GroupKind(), c.Name, allErrs) } diff --git a/api/v1alpha1/ippool_webhook_test.go b/api/v1alpha1/ippool_webhook_test.go index f58c3282..321938d1 100644 --- a/api/v1alpha1/ippool_webhook_test.go +++ b/api/v1alpha1/ippool_webhook_test.go @@ -20,36 +20,36 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func TestMetal3IPPoolDefault(t *testing.T) { +func TestIPPoolDefault(t *testing.T) { g := NewWithT(t) - c := &Metal3IPPool{ + c := &IPPool{ ObjectMeta: metav1.ObjectMeta{ Namespace: "foo", }, - Spec: Metal3IPPoolSpec{}, + Spec: IPPoolSpec{}, } c.Default() - g.Expect(c.Spec).To(Equal(Metal3IPPoolSpec{})) - g.Expect(c.Status).To(Equal(Metal3IPPoolStatus{})) + g.Expect(c.Spec).To(Equal(IPPoolSpec{})) + g.Expect(c.Status).To(Equal(IPPoolStatus{})) } -func TestMetal3IPPoolValidation(t *testing.T) { +func TestIPPoolValidation(t *testing.T) { tests := []struct { name string expectErr bool - c *Metal3IPPool + c *IPPool }{ { name: "should succeed when values and templates correct", expectErr: false, - c: &Metal3IPPool{ + c: &IPPool{ ObjectMeta: metav1.ObjectMeta{ Namespace: "foo", }, - Spec: Metal3IPPoolSpec{}, + Spec: IPPoolSpec{}, }, }, } @@ -69,33 +69,33 @@ func TestMetal3IPPoolValidation(t *testing.T) { } } -func TestMetal3IPPoolUpdateValidation(t *testing.T) { +func TestIPPoolUpdateValidation(t *testing.T) { tests := []struct { name string expectErr bool - newPool *Metal3IPPoolSpec - oldPool *Metal3IPPoolSpec + newPool *IPPoolSpec + oldPool *IPPoolSpec }{ { name: "should succeed when values and templates correct", expectErr: false, - newPool: &Metal3IPPoolSpec{}, - oldPool: &Metal3IPPoolSpec{}, + newPool: &IPPoolSpec{}, + oldPool: &IPPoolSpec{}, }, { name: "should fail when oldPool is nil", expectErr: true, - newPool: &Metal3IPPoolSpec{}, + newPool: &IPPoolSpec{}, oldPool: nil, }, { name: "should fail when namePrefix value changes", expectErr: true, - newPool: &Metal3IPPoolSpec{ + newPool: &IPPoolSpec{ NamePrefix: "abcde", }, - oldPool: &Metal3IPPoolSpec{ + oldPool: &IPPoolSpec{ NamePrefix: "abcd", }, }, @@ -103,9 +103,9 @@ func TestMetal3IPPoolUpdateValidation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - var newPool, oldPool *Metal3IPPool + var newPool, oldPool *IPPool g := NewWithT(t) - newPool = &Metal3IPPool{ + newPool = &IPPool{ ObjectMeta: metav1.ObjectMeta{ Namespace: "foo", }, @@ -113,7 +113,7 @@ func TestMetal3IPPoolUpdateValidation(t *testing.T) { } if tt.oldPool != nil { - oldPool = &Metal3IPPool{ + oldPool = &IPPool{ ObjectMeta: metav1.ObjectMeta{ Namespace: "foo", }, diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 04f2d2ad..49d17a90 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -26,60 +26,25 @@ import ( ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IPPool) DeepCopyInto(out *IPPool) { - *out = *in - if in.Start != nil { - in, out := &in.Start, &out.Start - *out = new(IPAddressStr) - **out = **in - } - if in.End != nil { - in, out := &in.End, &out.End - *out = new(IPAddressStr) - **out = **in - } - if in.Subnet != nil { - in, out := &in.Subnet, &out.Subnet - *out = new(IPSubnetStr) - **out = **in - } - if in.Gateway != nil { - in, out := &in.Gateway, &out.Gateway - *out = new(IPAddressStr) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPool. -func (in *IPPool) DeepCopy() *IPPool { - if in == nil { - return nil - } - out := new(IPPool) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Metal3IPAddress) DeepCopyInto(out *Metal3IPAddress) { +func (in *IPAddress) DeepCopyInto(out *IPAddress) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metal3IPAddress. -func (in *Metal3IPAddress) DeepCopy() *Metal3IPAddress { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddress. +func (in *IPAddress) DeepCopy() *IPAddress { if in == nil { return nil } - out := new(Metal3IPAddress) + out := new(IPAddress) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *Metal3IPAddress) DeepCopyObject() runtime.Object { +func (in *IPAddress) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -87,31 +52,31 @@ func (in *Metal3IPAddress) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Metal3IPAddressList) DeepCopyInto(out *Metal3IPAddressList) { +func (in *IPAddressList) DeepCopyInto(out *IPAddressList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]Metal3IPAddress, len(*in)) + *out = make([]IPAddress, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metal3IPAddressList. -func (in *Metal3IPAddressList) DeepCopy() *Metal3IPAddressList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddressList. +func (in *IPAddressList) DeepCopy() *IPAddressList { if in == nil { return nil } - out := new(Metal3IPAddressList) + out := new(IPAddressList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *Metal3IPAddressList) DeepCopyObject() runtime.Object { +func (in *IPAddressList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -119,7 +84,7 @@ func (in *Metal3IPAddressList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Metal3IPAddressSpec) DeepCopyInto(out *Metal3IPAddressSpec) { +func (in *IPAddressSpec) DeepCopyInto(out *IPAddressSpec) { *out = *in out.Claim = in.Claim out.Pool = in.Pool @@ -130,18 +95,18 @@ func (in *Metal3IPAddressSpec) DeepCopyInto(out *Metal3IPAddressSpec) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metal3IPAddressSpec. -func (in *Metal3IPAddressSpec) DeepCopy() *Metal3IPAddressSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddressSpec. +func (in *IPAddressSpec) DeepCopy() *IPAddressSpec { if in == nil { return nil } - out := new(Metal3IPAddressSpec) + out := new(IPAddressSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Metal3IPClaim) DeepCopyInto(out *Metal3IPClaim) { +func (in *IPClaim) DeepCopyInto(out *IPClaim) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) @@ -149,18 +114,18 @@ func (in *Metal3IPClaim) DeepCopyInto(out *Metal3IPClaim) { in.Status.DeepCopyInto(&out.Status) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metal3IPClaim. -func (in *Metal3IPClaim) DeepCopy() *Metal3IPClaim { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPClaim. +func (in *IPClaim) DeepCopy() *IPClaim { if in == nil { return nil } - out := new(Metal3IPClaim) + out := new(IPClaim) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *Metal3IPClaim) DeepCopyObject() runtime.Object { +func (in *IPClaim) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -168,31 +133,31 @@ func (in *Metal3IPClaim) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Metal3IPClaimList) DeepCopyInto(out *Metal3IPClaimList) { +func (in *IPClaimList) DeepCopyInto(out *IPClaimList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]Metal3IPClaim, len(*in)) + *out = make([]IPClaim, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metal3IPClaimList. -func (in *Metal3IPClaimList) DeepCopy() *Metal3IPClaimList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPClaimList. +func (in *IPClaimList) DeepCopy() *IPClaimList { if in == nil { return nil } - out := new(Metal3IPClaimList) + out := new(IPClaimList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *Metal3IPClaimList) DeepCopyObject() runtime.Object { +func (in *IPClaimList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -200,23 +165,23 @@ func (in *Metal3IPClaimList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Metal3IPClaimSpec) DeepCopyInto(out *Metal3IPClaimSpec) { +func (in *IPClaimSpec) DeepCopyInto(out *IPClaimSpec) { *out = *in out.Pool = in.Pool } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metal3IPClaimSpec. -func (in *Metal3IPClaimSpec) DeepCopy() *Metal3IPClaimSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPClaimSpec. +func (in *IPClaimSpec) DeepCopy() *IPClaimSpec { if in == nil { return nil } - out := new(Metal3IPClaimSpec) + out := new(IPClaimSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Metal3IPClaimStatus) DeepCopyInto(out *Metal3IPClaimStatus) { +func (in *IPClaimStatus) DeepCopyInto(out *IPClaimStatus) { *out = *in if in.Address != nil { in, out := &in.Address, &out.Address @@ -230,18 +195,18 @@ func (in *Metal3IPClaimStatus) DeepCopyInto(out *Metal3IPClaimStatus) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metal3IPClaimStatus. -func (in *Metal3IPClaimStatus) DeepCopy() *Metal3IPClaimStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPClaimStatus. +func (in *IPClaimStatus) DeepCopy() *IPClaimStatus { if in == nil { return nil } - out := new(Metal3IPClaimStatus) + out := new(IPClaimStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Metal3IPPool) DeepCopyInto(out *Metal3IPPool) { +func (in *IPPool) DeepCopyInto(out *IPPool) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) @@ -249,18 +214,18 @@ func (in *Metal3IPPool) DeepCopyInto(out *Metal3IPPool) { in.Status.DeepCopyInto(&out.Status) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metal3IPPool. -func (in *Metal3IPPool) DeepCopy() *Metal3IPPool { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPool. +func (in *IPPool) DeepCopy() *IPPool { if in == nil { return nil } - out := new(Metal3IPPool) + out := new(IPPool) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *Metal3IPPool) DeepCopyObject() runtime.Object { +func (in *IPPool) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -268,31 +233,31 @@ func (in *Metal3IPPool) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Metal3IPPoolList) DeepCopyInto(out *Metal3IPPoolList) { +func (in *IPPoolList) DeepCopyInto(out *IPPoolList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]Metal3IPPool, len(*in)) + *out = make([]IPPool, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metal3IPPoolList. -func (in *Metal3IPPoolList) DeepCopy() *Metal3IPPoolList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPoolList. +func (in *IPPoolList) DeepCopy() *IPPoolList { if in == nil { return nil } - out := new(Metal3IPPoolList) + out := new(IPPoolList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *Metal3IPPoolList) DeepCopyObject() runtime.Object { +func (in *IPPoolList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -300,11 +265,11 @@ func (in *Metal3IPPoolList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Metal3IPPoolSpec) DeepCopyInto(out *Metal3IPPoolSpec) { +func (in *IPPoolSpec) DeepCopyInto(out *IPPoolSpec) { *out = *in if in.Pools != nil { in, out := &in.Pools, &out.Pools - *out = make([]IPPool, len(*in)) + *out = make([]Pool, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -323,18 +288,18 @@ func (in *Metal3IPPoolSpec) DeepCopyInto(out *Metal3IPPoolSpec) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metal3IPPoolSpec. -func (in *Metal3IPPoolSpec) DeepCopy() *Metal3IPPoolSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPoolSpec. +func (in *IPPoolSpec) DeepCopy() *IPPoolSpec { if in == nil { return nil } - out := new(Metal3IPPoolSpec) + out := new(IPPoolSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Metal3IPPoolStatus) DeepCopyInto(out *Metal3IPPoolStatus) { +func (in *IPPoolStatus) DeepCopyInto(out *IPPoolStatus) { *out = *in if in.LastUpdated != nil { in, out := &in.LastUpdated, &out.LastUpdated @@ -349,12 +314,47 @@ func (in *Metal3IPPoolStatus) DeepCopyInto(out *Metal3IPPoolStatus) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metal3IPPoolStatus. -func (in *Metal3IPPoolStatus) DeepCopy() *Metal3IPPoolStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPoolStatus. +func (in *IPPoolStatus) DeepCopy() *IPPoolStatus { + if in == nil { + return nil + } + out := new(IPPoolStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Pool) DeepCopyInto(out *Pool) { + *out = *in + if in.Start != nil { + in, out := &in.Start, &out.Start + *out = new(IPAddressStr) + **out = **in + } + if in.End != nil { + in, out := &in.End, &out.End + *out = new(IPAddressStr) + **out = **in + } + if in.Subnet != nil { + in, out := &in.Subnet, &out.Subnet + *out = new(IPSubnetStr) + **out = **in + } + if in.Gateway != nil { + in, out := &in.Gateway, &out.Gateway + *out = new(IPAddressStr) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Pool. +func (in *Pool) DeepCopy() *Pool { if in == nil { return nil } - out := new(Metal3IPPoolStatus) + out := new(Pool) in.DeepCopyInto(out) return out } diff --git a/config/crd/bases/ipam.metal3.io_metal3ipaddresses.yaml b/config/crd/bases/ipam.metal3.io_ipaddresses.yaml similarity index 92% rename from config/crd/bases/ipam.metal3.io_metal3ipaddresses.yaml rename to config/crd/bases/ipam.metal3.io_ipaddresses.yaml index a4f0c84e..2e08ef3b 100644 --- a/config/crd/bases/ipam.metal3.io_metal3ipaddresses.yaml +++ b/config/crd/bases/ipam.metal3.io_ipaddresses.yaml @@ -6,26 +6,26 @@ metadata: annotations: controller-gen.kubebuilder.io/version: v0.2.8 creationTimestamp: null - name: metal3ipaddresses.ipam.metal3.io + name: ipaddresses.ipam.metal3.io spec: group: ipam.metal3.io names: categories: - - cluster-api - kind: Metal3IPAddress - listKind: Metal3IPAddressList - plural: metal3ipaddresses + - metal3 + kind: IPAddress + listKind: IPAddressList + plural: ipaddresses shortNames: - - m3ipa - - m3ipaddress - - metal3ipaddress - singular: metal3ipaddress + - ipa + - ipaddress + - ipaddress + singular: ipaddress scope: Namespaced versions: - name: v1alpha1 schema: openAPIV3Schema: - description: Metal3IPAddress is the Schema for the metal3ipaddresses API + description: IPAddress is the Schema for the ipaddresses API properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -40,15 +40,14 @@ spec: metadata: type: object spec: - description: Metal3IPAddressSpec defines the desired state of Metal3IPAddress. + description: IPAddressSpec defines the desired state of IPAddress. properties: address: description: Address contains the IP address pattern: ((^((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$)|(^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$)) type: string claim: - description: Claim points to the object the Metal3IPClaim was created - for. + description: Claim points to the object the IPClaim was created for. properties: apiVersion: description: API version of the referent. @@ -88,7 +87,7 @@ spec: pattern: ((^((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$)|(^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$)) type: string pool: - description: Pool is the Metal3IPPool this was generated from. + description: Pool is the IPPool this was generated from. properties: apiVersion: description: API version of the referent. diff --git a/config/crd/bases/ipam.metal3.io_metal3ipclaims.yaml b/config/crd/bases/ipam.metal3.io_ipclaims.yaml similarity index 91% rename from config/crd/bases/ipam.metal3.io_metal3ipclaims.yaml rename to config/crd/bases/ipam.metal3.io_ipclaims.yaml index 9cb2f7b6..be527693 100644 --- a/config/crd/bases/ipam.metal3.io_metal3ipclaims.yaml +++ b/config/crd/bases/ipam.metal3.io_ipclaims.yaml @@ -6,26 +6,26 @@ metadata: annotations: controller-gen.kubebuilder.io/version: v0.2.8 creationTimestamp: null - name: metal3ipclaims.ipam.metal3.io + name: ipclaims.ipam.metal3.io spec: group: ipam.metal3.io names: categories: - cluster-api - kind: Metal3IPClaim - listKind: Metal3IPClaimList - plural: metal3ipclaims + kind: IPClaim + listKind: IPClaimList + plural: ipclaims shortNames: - m3ipc - m3ipclaim - - metal3ipclaim - singular: metal3ipclaim + - ipclaim + singular: ipclaim scope: Namespaced versions: - name: v1alpha1 schema: openAPIV3Schema: - description: Metal3IPClaim is the Schema for the metal3ipclaims API + description: IPClaim is the Schema for the ipclaims API properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -40,10 +40,10 @@ spec: metadata: type: object spec: - description: Metal3IPClaimSpec defines the desired state of Metal3IPClaim. + description: IPClaimSpec defines the desired state of IPClaim. properties: pool: - description: Pool is the Metal3IPPool this was generated from. + description: Pool is the IPPool this was generated from. properties: apiVersion: description: API version of the referent. @@ -82,11 +82,11 @@ spec: - pool type: object status: - description: Metal3IPClaimStatus defines the observed state of Metal3IPClaim. + description: IPClaimStatus defines the observed state of IPClaim. properties: address: - description: Address is the Metal3IPAddress that was generated for - this claim. + description: Address is the IPAddress that was generated for this + claim. properties: apiVersion: description: API version of the referent. diff --git a/config/crd/bases/ipam.metal3.io_metal3ippools.yaml b/config/crd/bases/ipam.metal3.io_ippools.yaml similarity index 95% rename from config/crd/bases/ipam.metal3.io_metal3ippools.yaml rename to config/crd/bases/ipam.metal3.io_ippools.yaml index 0679aa17..aea5483d 100644 --- a/config/crd/bases/ipam.metal3.io_metal3ippools.yaml +++ b/config/crd/bases/ipam.metal3.io_ippools.yaml @@ -6,19 +6,19 @@ metadata: annotations: controller-gen.kubebuilder.io/version: v0.2.8 creationTimestamp: null - name: metal3ippools.ipam.metal3.io + name: ippools.ipam.metal3.io spec: group: ipam.metal3.io names: categories: - cluster-api - kind: Metal3IPPool - listKind: Metal3IPPoolList - plural: metal3ippools + kind: IPPool + listKind: IPPoolList + plural: ippools shortNames: - m3ipp - m3ippool - singular: metal3ippool + singular: ippool scope: Namespaced versions: - additionalPrinterColumns: @@ -29,7 +29,7 @@ spec: name: v1alpha1 schema: openAPIV3Schema: - description: Metal3IPPool is the Schema for the metal3ippools API + description: IPPool is the Schema for the ippools API properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -44,7 +44,7 @@ spec: metadata: type: object spec: - description: Metal3IPPoolSpec defines the desired state of Metal3IPPool. + description: IPPoolSpec defines the desired state of IPPool. properties: clusterName: description: ClusterName is the name of the Cluster this object belongs @@ -56,7 +56,7 @@ spec: pattern: ((^((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$)|(^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$)) type: string namePrefix: - description: namePrefix is the prefix used to generate the Metal3IPAddress + description: namePrefix is the prefix used to generate the IPAddress object names minLength: 1 type: string @@ -109,7 +109,7 @@ spec: - namePrefix type: object status: - description: Metal3IPPoolStatus defines the observed state of Metal3IPPool. + description: IPPoolStatus defines the observed state of IPPool. properties: indexes: additionalProperties: diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index bba9f9b4..1cd5dbf8 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -9,42 +9,24 @@ commonLabels: cluster.x-k8s.io/v1alpha3: v1alpha3_v1alpha4 resources: -- bases/infrastructure.cluster.x-k8s.io_metal3clusters.yaml -- bases/infrastructure.cluster.x-k8s.io_metal3machines.yaml -- bases/infrastructure.cluster.x-k8s.io_metal3machinetemplates.yaml -- bases/infrastructure.cluster.x-k8s.io_metal3datatemplates.yaml -- bases/infrastructure.cluster.x-k8s.io_metal3datas.yaml -- bases/infrastructure.cluster.x-k8s.io_metal3dataclaims.yaml -- bases/infrastructure.cluster.x-k8s.io_metal3ippools.yaml -- bases/infrastructure.cluster.x-k8s.io_metal3ipaddresses.yaml -- bases/infrastructure.cluster.x-k8s.io_metal3ipclaims.yaml +- bases/ipam.metal3.io_ippools.yaml +- bases/ipam.metal3.io_ipaddresses.yaml +- bases/ipam.metal3.io_ipclaims.yaml # +kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD -- patches/webhook_in_metal3machines.yaml -- patches/webhook_in_metal3clusters.yaml -- patches/webhook_in_metal3machinetemplates.yaml -- patches/webhook_in_metal3datatemplates.yaml -- patches/webhook_in_metal3datas.yaml -- patches/webhook_in_metal3dataclaims.yaml -- patches/webhook_in_metal3ippools.yaml -- patches/webhook_in_metal3ipaddresses.yaml -- patches/webhook_in_metal3ipclaims.yaml +- patches/webhook_in_ippools.yaml +- patches/webhook_in_ipaddresses.yaml +- patches/webhook_in_ipclaims.yaml # +kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. # patches here are for enabling the CA injection for each CRD -- patches/cainjection_in_metal3machines.yaml -- patches/cainjection_in_metal3clusters.yaml -- patches/cainjection_in_metal3machinetemplates.yaml -- patches/cainjection_in_metal3datatemplates.yaml -- patches/cainjection_in_metal3datas.yaml -- patches/cainjection_in_metal3dataclaims.yaml -- patches/cainjection_in_metal3ippools.yaml -- patches/cainjection_in_metal3ipaddresses.yaml -- patches/cainjection_in_metal3ipclaims.yaml +- patches/cainjection_in_ippools.yaml +- patches/cainjection_in_ipaddresses.yaml +- patches/cainjection_in_ipclaims.yaml # +kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_metal3datas.yaml b/config/crd/patches/cainjection_in_ipaddresses.yaml similarity index 85% rename from config/crd/patches/cainjection_in_metal3datas.yaml rename to config/crd/patches/cainjection_in_ipaddresses.yaml index 3dc0ef85..136657db 100644 --- a/config/crd/patches/cainjection_in_metal3datas.yaml +++ b/config/crd/patches/cainjection_in_ipaddresses.yaml @@ -5,4 +5,4 @@ kind: CustomResourceDefinition metadata: annotations: cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: metal3datas.infrastructure.cluster.x-k8s.io + name: ipaddresses.ipam.metal3.io diff --git a/config/crd/patches/cainjection_in_metal3ippools.yaml b/config/crd/patches/cainjection_in_ipclaims.yaml similarity index 84% rename from config/crd/patches/cainjection_in_metal3ippools.yaml rename to config/crd/patches/cainjection_in_ipclaims.yaml index cd75a055..dd95cb46 100644 --- a/config/crd/patches/cainjection_in_metal3ippools.yaml +++ b/config/crd/patches/cainjection_in_ipclaims.yaml @@ -5,4 +5,4 @@ kind: CustomResourceDefinition metadata: annotations: cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: metal3ippools.infrastructure.cluster.x-k8s.io + name: ipclaims.ipam.metal3.io diff --git a/config/crd/patches/cainjection_in_metal3ipclaims.yaml b/config/crd/patches/cainjection_in_ippools.yaml similarity index 84% rename from config/crd/patches/cainjection_in_metal3ipclaims.yaml rename to config/crd/patches/cainjection_in_ippools.yaml index ea60aa25..8754c15b 100644 --- a/config/crd/patches/cainjection_in_metal3ipclaims.yaml +++ b/config/crd/patches/cainjection_in_ippools.yaml @@ -5,4 +5,4 @@ kind: CustomResourceDefinition metadata: annotations: cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: metal3ipclaims.infrastructure.cluster.x-k8s.io + name: ippools.ipam.metal3.io diff --git a/config/crd/patches/cainjection_in_metal3clusters.yaml b/config/crd/patches/cainjection_in_metal3clusters.yaml deleted file mode 100644 index f6d17861..00000000 --- a/config/crd/patches/cainjection_in_metal3clusters.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# The following patch adds a directive for certmanager to inject CA into the CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: metal3clusters.infrastructure.cluster.x-k8s.io diff --git a/config/crd/patches/cainjection_in_metal3dataclaims.yaml b/config/crd/patches/cainjection_in_metal3dataclaims.yaml deleted file mode 100644 index 7faf0132..00000000 --- a/config/crd/patches/cainjection_in_metal3dataclaims.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# The following patch adds a directive for certmanager to inject CA into the CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: metal3dataclaims.infrastructure.cluster.x-k8s.io diff --git a/config/crd/patches/cainjection_in_metal3datatemplates.yaml b/config/crd/patches/cainjection_in_metal3datatemplates.yaml deleted file mode 100644 index 025377e9..00000000 --- a/config/crd/patches/cainjection_in_metal3datatemplates.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# The following patch adds a directive for certmanager to inject CA into the CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: metal3datatemplates.infrastructure.cluster.x-k8s.io diff --git a/config/crd/patches/cainjection_in_metal3ipaddresses.yaml b/config/crd/patches/cainjection_in_metal3ipaddresses.yaml deleted file mode 100644 index 07686077..00000000 --- a/config/crd/patches/cainjection_in_metal3ipaddresses.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# The following patch adds a directive for certmanager to inject CA into the CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: metal3ipaddresses.infrastructure.cluster.x-k8s.io diff --git a/config/crd/patches/cainjection_in_metal3machines.yaml b/config/crd/patches/cainjection_in_metal3machines.yaml deleted file mode 100644 index 5e0641ce..00000000 --- a/config/crd/patches/cainjection_in_metal3machines.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# The following patch adds a directive for certmanager to inject CA into the CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: metal3machines.infrastructure.cluster.x-k8s.io diff --git a/config/crd/patches/cainjection_in_metal3machinetemplates.yaml b/config/crd/patches/cainjection_in_metal3machinetemplates.yaml deleted file mode 100644 index b4ebc7eb..00000000 --- a/config/crd/patches/cainjection_in_metal3machinetemplates.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# The following patch adds a directive for certmanager to inject CA into the CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: metal3machinetemplates.infrastructure.cluster.x-k8s.io diff --git a/config/crd/patches/webhook_in_metal3datas.yaml b/config/crd/patches/webhook_in_ipaddresses.yaml similarity index 92% rename from config/crd/patches/webhook_in_metal3datas.yaml rename to config/crd/patches/webhook_in_ipaddresses.yaml index 5ecf4e89..787de167 100644 --- a/config/crd/patches/webhook_in_metal3datas.yaml +++ b/config/crd/patches/webhook_in_ipaddresses.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: - name: metal3datas.infrastructure.cluster.x-k8s.io + name: ipaddresses.ipam.metal3.io spec: conversion: strategy: Webhook diff --git a/config/crd/patches/webhook_in_metal3ippools.yaml b/config/crd/patches/webhook_in_ipclaims.yaml similarity index 92% rename from config/crd/patches/webhook_in_metal3ippools.yaml rename to config/crd/patches/webhook_in_ipclaims.yaml index e986e8de..5a3ec4e8 100644 --- a/config/crd/patches/webhook_in_metal3ippools.yaml +++ b/config/crd/patches/webhook_in_ipclaims.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: - name: metal3ippools.infrastructure.cluster.x-k8s.io + name: ipclaims.ipam.metal3.io spec: conversion: strategy: Webhook diff --git a/config/crd/patches/webhook_in_metal3clusters.yaml b/config/crd/patches/webhook_in_ippools.yaml similarity index 92% rename from config/crd/patches/webhook_in_metal3clusters.yaml rename to config/crd/patches/webhook_in_ippools.yaml index da9789dd..da16f11e 100644 --- a/config/crd/patches/webhook_in_metal3clusters.yaml +++ b/config/crd/patches/webhook_in_ippools.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: - name: metal3clusters.infrastructure.cluster.x-k8s.io + name: ippools.ipam.metal3.io spec: conversion: strategy: Webhook diff --git a/config/crd/patches/webhook_in_metal3dataclaims.yaml b/config/crd/patches/webhook_in_metal3dataclaims.yaml deleted file mode 100644 index ac2e73e3..00000000 --- a/config/crd/patches/webhook_in_metal3dataclaims.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# The following patch enables conversion webhook for CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: metal3dataclaims.infrastructure.cluster.x-k8s.io -spec: - conversion: - strategy: Webhook - webhook: - conversionReviewVersions: ["v1", "v1beta1"] - clientConfig: - # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, - # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) - caBundle: Cg== - service: - namespace: system - name: webhook-service - path: /convert diff --git a/config/crd/patches/webhook_in_metal3datatemplates.yaml b/config/crd/patches/webhook_in_metal3datatemplates.yaml deleted file mode 100644 index eb65c21d..00000000 --- a/config/crd/patches/webhook_in_metal3datatemplates.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# The following patch enables conversion webhook for CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: metal3datatemplates.infrastructure.cluster.x-k8s.io -spec: - conversion: - strategy: Webhook - webhook: - conversionReviewVersions: ["v1", "v1beta1"] - clientConfig: - # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, - # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) - caBundle: Cg== - service: - namespace: system - name: webhook-service - path: /convert diff --git a/config/crd/patches/webhook_in_metal3ipaddresses.yaml b/config/crd/patches/webhook_in_metal3ipaddresses.yaml deleted file mode 100644 index f9c65864..00000000 --- a/config/crd/patches/webhook_in_metal3ipaddresses.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# The following patch enables conversion webhook for CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: metal3ipaddresses.infrastructure.cluster.x-k8s.io -spec: - conversion: - strategy: Webhook - webhook: - conversionReviewVersions: ["v1", "v1beta1"] - clientConfig: - # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, - # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) - caBundle: Cg== - service: - namespace: system - name: webhook-service - path: /convert diff --git a/config/crd/patches/webhook_in_metal3ipclaims.yaml b/config/crd/patches/webhook_in_metal3ipclaims.yaml deleted file mode 100644 index 7384e03e..00000000 --- a/config/crd/patches/webhook_in_metal3ipclaims.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# The following patch enables conversion webhook for CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: metal3ipclaims.infrastructure.cluster.x-k8s.io -spec: - conversion: - strategy: Webhook - webhook: - conversionReviewVersions: ["v1", "v1beta1"] - clientConfig: - # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, - # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) - caBundle: Cg== - service: - namespace: system - name: webhook-service - path: /convert diff --git a/config/crd/patches/webhook_in_metal3machines.yaml b/config/crd/patches/webhook_in_metal3machines.yaml deleted file mode 100644 index 7097af26..00000000 --- a/config/crd/patches/webhook_in_metal3machines.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# The following patch enables conversion webhook for CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: metal3machines.infrastructure.cluster.x-k8s.io -spec: - conversion: - strategy: Webhook - webhook: - conversionReviewVersions: ["v1", "v1beta1"] - clientConfig: - # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, - # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) - caBundle: Cg== - service: - namespace: system - name: webhook-service - path: /convert diff --git a/config/crd/patches/webhook_in_metal3machinetemplates.yaml b/config/crd/patches/webhook_in_metal3machinetemplates.yaml deleted file mode 100644 index 100c9105..00000000 --- a/config/crd/patches/webhook_in_metal3machinetemplates.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# The following patch enables conversion webhook for CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: metal3machinetemplates.infrastructure.cluster.x-k8s.io -spec: - conversion: - strategy: Webhook - webhook: - conversionReviewVersions: ["v1", "v1beta1"] - clientConfig: - # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, - # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) - caBundle: Cg== - service: - namespace: system - name: webhook-service - path: /convert diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index b66a7ba2..fcca2ece 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -1,7 +1,8 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -# Adds namespace to all resources. +# Adds namespace to all resources. Keep it in capm3-system, as it is a +# dependency for CAPM3 namespace: capm3-system resources: diff --git a/config/kustomization.yaml b/config/kustomization.yaml index a2689560..cea7b98a 100644 --- a/config/kustomization.yaml +++ b/config/kustomization.yaml @@ -1,4 +1,4 @@ -namePrefix: capm3- +namePrefix: metal3-ipam- commonLabels: cluster.x-k8s.io/provider: "infrastructure-metal3" @@ -9,57 +9,21 @@ bases: - default patchesJson6902: -- target: # NOTE: This patch needs to be repeatd for EACH CustomResourceDefinition you have under crd/bases. - group: apiextensions.k8s.io - version: v1 - kind: CustomResourceDefinition - name: metal3clusters.infrastructure.cluster.x-k8s.io - path: patch_crd_webhook_namespace.yaml -- target: - group: apiextensions.k8s.io - version: v1 - kind: CustomResourceDefinition - name: metal3machines.infrastructure.cluster.x-k8s.io - path: patch_crd_webhook_namespace.yaml -- target: - group: apiextensions.k8s.io - version: v1 - kind: CustomResourceDefinition - name: metal3machinetemplates.infrastructure.cluster.x-k8s.io - path: patch_crd_webhook_namespace.yaml -- target: - group: apiextensions.k8s.io - version: v1 - kind: CustomResourceDefinition - name: metal3datatemplates.infrastructure.cluster.x-k8s.io - path: patch_crd_webhook_namespace.yaml -- target: - group: apiextensions.k8s.io - version: v1 - kind: CustomResourceDefinition - name: metal3datas.infrastructure.cluster.x-k8s.io - path: patch_crd_webhook_namespace.yaml -- target: - group: apiextensions.k8s.io - version: v1 - kind: CustomResourceDefinition - name: metal3dataclaims.infrastructure.cluster.x-k8s.io - path: patch_crd_webhook_namespace.yaml - target: group: apiextensions.k8s.io version: v1 kind: CustomResourceDefinition - name: metal3ippools.infrastructure.cluster.x-k8s.io + name: ippools.ipam.metal3.io path: patch_crd_webhook_namespace.yaml - target: group: apiextensions.k8s.io version: v1 kind: CustomResourceDefinition - name: metal3ipaddresses.infrastructure.cluster.x-k8s.io + name: ipaddresses.ipam.metal3.io path: patch_crd_webhook_namespace.yaml - target: group: apiextensions.k8s.io version: v1 kind: CustomResourceDefinition - name: metal3ipclaims.infrastructure.cluster.x-k8s.io + name: ipclaims.ipam.metal3.io path: patch_crd_webhook_namespace.yaml diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 0671c61d..2c9d97d7 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -37,15 +37,6 @@ rules: - get - list - watch -- apiGroups: - - cluster.x-k8s.io - resources: - - clusters - - clusters/status - verbs: - - get - - list - - watch - apiGroups: - cluster.x-k8s.io resources: @@ -53,138 +44,9 @@ rules: verbs: - get - apiGroups: - - cluster.x-k8s.io - resources: - - machines - - machines/status - verbs: - - get - - list - - watch -- apiGroups: - - infrastructure.cluster.x-k8s.io - resources: - - metal3clusters - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - infrastructure.cluster.x-k8s.io - resources: - - metal3clusters/status - verbs: - - get - - patch - - update -- apiGroups: - - infrastructure.cluster.x-k8s.io - resources: - - metal3dataclaims - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - infrastructure.cluster.x-k8s.io - resources: - - metal3dataclaims/status - verbs: - - get - - patch - - update -- apiGroups: - - infrastructure.cluster.x-k8s.io - resources: - - metal3datas - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - infrastructure.cluster.x-k8s.io - resources: - - metal3datas/status - verbs: - - get - - patch - - update -- apiGroups: - - infrastructure.cluster.x-k8s.io - resources: - - metal3datatemplates - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - infrastructure.cluster.x-k8s.io - resources: - - metal3datatemplates/status - verbs: - - get - - patch - - update -- apiGroups: - - infrastructure.cluster.x-k8s.io - resources: - - metal3ipaddresses - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - infrastructure.cluster.x-k8s.io - resources: - - metal3ipaddresses/status - verbs: - - get - - patch - - update -- apiGroups: - - infrastructure.cluster.x-k8s.io - resources: - - metal3ipclaims - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - infrastructure.cluster.x-k8s.io - resources: - - metal3ipclaims/status - verbs: - - get - - patch - - update -- apiGroups: - - infrastructure.cluster.x-k8s.io + - ipam.metal3.io resources: - - metal3ippools + - ipaddresses verbs: - create - delete @@ -194,17 +56,17 @@ rules: - update - watch - apiGroups: - - infrastructure.cluster.x-k8s.io + - ipam.metal3.io resources: - - metal3ippools/status + - ipaddresses/status verbs: - get - patch - update - apiGroups: - - infrastructure.cluster.x-k8s.io + - ipam.metal3.io resources: - - metal3machines + - ipclaims verbs: - create - delete @@ -214,17 +76,17 @@ rules: - update - watch - apiGroups: - - infrastructure.cluster.x-k8s.io + - ipam.metal3.io resources: - - metal3machines/status + - ipclaims/status verbs: - get - patch - update - apiGroups: - - metal3.io + - ipam.metal3.io resources: - - baremetalhosts + - ippools verbs: - create - delete @@ -234,9 +96,9 @@ rules: - update - watch - apiGroups: - - metal3.io + - ipam.metal3.io resources: - - baremetalhosts/status + - ippools/status verbs: - get - patch diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 3455451e..4dc467b7 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -11,58 +11,58 @@ webhooks: service: name: webhook-service namespace: system - path: /mutate-infrastructure-cluster-x-k8s-io-v1alpha4-metal3ipaddress + path: /mutate-infrastructure-cluster-x-k8s-io-v1alpha4-ipaddress failurePolicy: Fail matchPolicy: Equivalent - name: default.metal3ipaddress.infrastructure.cluster.x-k8s.io + name: default.ipaddress.ipam.metal3.io rules: - apiGroups: - - infrastructure.cluster.x-k8s.io + - ipam.metal3.io apiVersions: - v1alpha4 operations: - CREATE - UPDATE resources: - - metal3ipaddresses + - ipaddresses - clientConfig: caBundle: Cg== service: name: webhook-service namespace: system - path: /mutate-infrastructure-cluster-x-k8s-io-v1alpha4-metal3ipclaim + path: /mutate-infrastructure-cluster-x-k8s-io-v1alpha4-ipclaim failurePolicy: Fail matchPolicy: Equivalent - name: default.metal3ipclaim.infrastructure.cluster.x-k8s.io + name: default.ipclaim.ipam.metal3.io rules: - apiGroups: - - infrastructure.cluster.x-k8s.io + - ipam.metal3.io apiVersions: - v1alpha4 operations: - CREATE - UPDATE resources: - - metal3ipclaims + - ipclaims - clientConfig: caBundle: Cg== service: name: webhook-service namespace: system - path: /mutate-infrastructure-cluster-x-k8s-io-v1alpha4-metal3ippool + path: /mutate-infrastructure-cluster-x-k8s-io-v1alpha4-ippool failurePolicy: Fail matchPolicy: Equivalent - name: default.metal3ippool.infrastructure.cluster.x-k8s.io + name: default.ippool.ipam.metal3.io rules: - apiGroups: - - infrastructure.cluster.x-k8s.io + - ipam.metal3.io apiVersions: - v1alpha4 operations: - CREATE - UPDATE resources: - - metal3ippools + - ippools --- apiVersion: admissionregistration.k8s.io/v1beta1 @@ -76,55 +76,55 @@ webhooks: service: name: webhook-service namespace: system - path: /validate-infrastructure-cluster-x-k8s-io-v1alpha4-metal3ipaddress + path: /validate-infrastructure-cluster-x-k8s-io-v1alpha4-ipaddress failurePolicy: Fail matchPolicy: Equivalent - name: validation.metal3ipaddress.infrastructure.cluster.x-k8s.io + name: validation.ipaddress.ipam.metal3.io rules: - apiGroups: - - infrastructure.cluster.x-k8s.io + - ipam.metal3.io apiVersions: - v1alpha4 operations: - CREATE - UPDATE resources: - - metal3ipaddresses + - ipaddresses - clientConfig: caBundle: Cg== service: name: webhook-service namespace: system - path: /validate-infrastructure-cluster-x-k8s-io-v1alpha4-metal3ipclaim + path: /validate-infrastructure-cluster-x-k8s-io-v1alpha4-ipclaim failurePolicy: Fail matchPolicy: Equivalent - name: validation.metal3ipclaim.infrastructure.cluster.x-k8s.io + name: validation.ipclaim.ipam.metal3.io rules: - apiGroups: - - infrastructure.cluster.x-k8s.io + - ipam.metal3.io apiVersions: - v1alpha4 operations: - CREATE - UPDATE resources: - - metal3ipclaims + - ipclaims - clientConfig: caBundle: Cg== service: name: webhook-service namespace: system - path: /validate-infrastructure-cluster-x-k8s-io-v1alpha4-metal3ippool + path: /validate-infrastructure-cluster-x-k8s-io-v1alpha4-ippool failurePolicy: Fail matchPolicy: Equivalent - name: validation.metal3ippool.infrastructure.cluster.x-k8s.io + name: validation.ippool.ipam.metal3.io rules: - apiGroups: - - infrastructure.cluster.x-k8s.io + - ipam.metal3.io apiVersions: - v1alpha4 operations: - CREATE - UPDATE resources: - - metal3ippools + - ippools diff --git a/controllers/metal3ippool_controller.go b/controllers/ippool_controller.go similarity index 75% rename from controllers/metal3ippool_controller.go rename to controllers/ippool_controller.go index a63d88e6..914109f1 100644 --- a/controllers/metal3ippool_controller.go +++ b/controllers/ippool_controller.go @@ -39,35 +39,35 @@ import ( ) const ( - ipPoolControllerName = "Metal3IPPool-controller" + ipPoolControllerName = "IPPool-controller" requeueAfter = time.Second * 30 ) -// Metal3IPPoolReconciler reconciles a Metal3IPPool object -type Metal3IPPoolReconciler struct { +// IPPoolReconciler reconciles a IPPool object +type IPPoolReconciler struct { Client client.Client ManagerFactory ipam.ManagerFactoryInterface Log logr.Logger } -// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=metal3ippools,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=metal3ippools/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=metal3ipclaims,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=metal3ipclaims/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=metal3ipaddresses,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=metal3ipaddresses/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=ipam.metal3.io,resources=ippools,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=ipam.metal3.io,resources=ippools/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=ipam.metal3.io,resources=ipclaims,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=ipam.metal3.io,resources=ipclaims/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=ipam.metal3.io,resources=ipaddresses,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=ipam.metal3.io,resources=ipaddresses/status,verbs=get;update;patch // +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters,verbs=get;list;watch // +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters/status,verbs=get // +kubebuilder:rbac:groups="",resources=events,verbs=get;list;watch;create;update;patch // +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete // Reconcile handles Metal3Machine events -func (r *Metal3IPPoolReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, rerr error) { +func (r *IPPoolReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, rerr error) { ctx := context.Background() metadataLog := r.Log.WithName(ipPoolControllerName).WithValues("metal3-ippool", req.NamespacedName) - // Fetch the Metal3IPPool instance. - ipamv1IPPool := &ipamv1.Metal3IPPool{} + // Fetch the IPPool instance. + ipamv1IPPool := &ipamv1.IPPool{} if err := r.Client.Get(ctx, req.NamespacedName, ipamv1IPPool); err != nil { if apierrors.IsNotFound(err) { @@ -79,7 +79,7 @@ func (r *Metal3IPPoolReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, rer if err != nil { return ctrl.Result{}, errors.Wrap(err, "failed to init patch helper") } - // Always patch ipamv1IPPool exiting this function so we can persist any Metal3IPPool changes. + // Always patch ipamv1IPPool exiting this function so we can persist any IPPool changes. defer func() { err := helper.Patch(ctx, ipamv1IPPool) if err != nil { @@ -134,10 +134,10 @@ func (r *Metal3IPPoolReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, rer return r.reconcileNormal(ctx, ipPoolMgr) } -func (r *Metal3IPPoolReconciler) reconcileNormal(ctx context.Context, +func (r *IPPoolReconciler) reconcileNormal(ctx context.Context, ipPoolMgr ipam.IPPoolManagerInterface, ) (ctrl.Result, error) { - // If the Metal3IPPool doesn't have finalizer, add it. + // If the IPPool doesn't have finalizer, add it. ipPoolMgr.SetFinalizer() _, err := ipPoolMgr.UpdateAddresses(ctx) @@ -148,7 +148,7 @@ func (r *Metal3IPPoolReconciler) reconcileNormal(ctx context.Context, return ctrl.Result{}, nil } -func (r *Metal3IPPoolReconciler) reconcileDelete(ctx context.Context, +func (r *IPPoolReconciler) reconcileDelete(ctx context.Context, ipPoolMgr ipam.IPPoolManagerInterface, ) (ctrl.Result, error) { @@ -158,7 +158,7 @@ func (r *Metal3IPPoolReconciler) reconcileDelete(ctx context.Context, } if allocationsNb == 0 { - // metal3ippool is marked for deletion and ready to be deleted, + // ippool is marked for deletion and ready to be deleted, // so remove the finalizer. ipPoolMgr.UnsetFinalizer() } @@ -167,13 +167,13 @@ func (r *Metal3IPPoolReconciler) reconcileDelete(ctx context.Context, } // SetupWithManager will add watches for this controller -func (r *Metal3IPPoolReconciler) SetupWithManager(mgr ctrl.Manager) error { +func (r *IPPoolReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&ipamv1.Metal3IPPool{}). + For(&ipamv1.IPPool{}). Watches( - &source.Kind{Type: &ipamv1.Metal3IPClaim{}}, + &source.Kind{Type: &ipamv1.IPClaim{}}, &handler.EnqueueRequestsFromMapFunc{ - ToRequests: handler.ToRequestsFunc(r.Metal3IPClaimToMetal3IPPool), + ToRequests: handler.ToRequestsFunc(r.IPClaimToIPPool), }, // Do not trigger a reconciliation on updates of the claim, as the Spec // fields are immutable @@ -187,11 +187,11 @@ func (r *Metal3IPPoolReconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } -// Metal3IPClaimToMetal3IPPool will return a reconcile request for a +// IPClaimToIPPool will return a reconcile request for a // Metal3DataTemplate if the event is for a -// Metal3IPClaim and that Metal3IPClaim references a Metal3DataTemplate -func (r *Metal3IPPoolReconciler) Metal3IPClaimToMetal3IPPool(obj handler.MapObject) []ctrl.Request { - if m3ipc, ok := obj.Object.(*ipamv1.Metal3IPClaim); ok { +// IPClaim and that IPClaim references a Metal3DataTemplate +func (r *IPPoolReconciler) IPClaimToIPPool(obj handler.MapObject) []ctrl.Request { + if m3ipc, ok := obj.Object.(*ipamv1.IPClaim); ok { if m3ipc.Spec.Pool.Name != "" { namespace := m3ipc.Spec.Pool.Namespace if namespace == "" { diff --git a/controllers/metal3ippool_controller_test.go b/controllers/ippool_controller_test.go similarity index 86% rename from controllers/metal3ippool_controller_test.go rename to controllers/ippool_controller_test.go index bd068cd2..f99eb08e 100644 --- a/controllers/metal3ippool_controller_test.go +++ b/controllers/ippool_controller_test.go @@ -47,13 +47,13 @@ var ( } ) -var _ = Describe("Metal3IPPool controller", func() { +var _ = Describe("IPPool controller", func() { type testCaseReconcile struct { expectError bool expectRequeue bool expectManager bool - m3ipp *ipamv1.Metal3IPPool + m3ipp *ipamv1.IPPool cluster *capi.Cluster managerError bool reconcileNormal bool @@ -108,7 +108,7 @@ var _ = Describe("Metal3IPPool controller", func() { } } - ipPoolReconcile := &Metal3IPPoolReconciler{ + ipPoolReconcile := &IPPoolReconciler{ Client: c, ManagerFactory: f, Log: klogr.New(), @@ -135,41 +135,41 @@ var _ = Describe("Metal3IPPool controller", func() { } gomockCtrl.Finish() }, - Entry("Metal3IPPool not found", testCaseReconcile{}), + Entry("IPPool not found", testCaseReconcile{}), Entry("Cluster not found", testCaseReconcile{ - m3ipp: &ipamv1.Metal3IPPool{ + m3ipp: &ipamv1.IPPool{ ObjectMeta: testObjectMeta, - Spec: ipamv1.Metal3IPPoolSpec{ClusterName: "abc"}, + Spec: ipamv1.IPPoolSpec{ClusterName: "abc"}, }, }), Entry("Deletion, Cluster not found", testCaseReconcile{ - m3ipp: &ipamv1.Metal3IPPool{ + m3ipp: &ipamv1.IPPool{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: "myns", DeletionTimestamp: ×tampNow, }, - Spec: ipamv1.Metal3IPPoolSpec{ClusterName: "abc"}, + Spec: ipamv1.IPPoolSpec{ClusterName: "abc"}, }, expectManager: true, }), Entry("Deletion, Cluster not found, error", testCaseReconcile{ - m3ipp: &ipamv1.Metal3IPPool{ + m3ipp: &ipamv1.IPPool{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: "myns", DeletionTimestamp: ×tampNow, }, - Spec: ipamv1.Metal3IPPoolSpec{ClusterName: "abc"}, + Spec: ipamv1.IPPoolSpec{ClusterName: "abc"}, }, expectManager: true, reconcileDeleteError: true, expectError: true, }), Entry("Paused cluster", testCaseReconcile{ - m3ipp: &ipamv1.Metal3IPPool{ + m3ipp: &ipamv1.IPPool{ ObjectMeta: testObjectMeta, - Spec: ipamv1.Metal3IPPoolSpec{ClusterName: "abc"}, + Spec: ipamv1.IPPoolSpec{ClusterName: "abc"}, }, cluster: &capi.Cluster{ ObjectMeta: testObjectMeta, @@ -181,9 +181,9 @@ var _ = Describe("Metal3IPPool controller", func() { expectManager: true, }), Entry("Error in manager", testCaseReconcile{ - m3ipp: &ipamv1.Metal3IPPool{ + m3ipp: &ipamv1.IPPool{ ObjectMeta: testObjectMeta, - Spec: ipamv1.Metal3IPPoolSpec{ClusterName: "abc"}, + Spec: ipamv1.IPPoolSpec{ClusterName: "abc"}, }, cluster: &capi.Cluster{ ObjectMeta: testObjectMeta, @@ -191,9 +191,9 @@ var _ = Describe("Metal3IPPool controller", func() { managerError: true, }), Entry("Reconcile normal error", testCaseReconcile{ - m3ipp: &ipamv1.Metal3IPPool{ + m3ipp: &ipamv1.IPPool{ ObjectMeta: testObjectMeta, - Spec: ipamv1.Metal3IPPoolSpec{ClusterName: "abc"}, + Spec: ipamv1.IPPoolSpec{ClusterName: "abc"}, }, cluster: &capi.Cluster{ ObjectMeta: testObjectMeta, @@ -203,17 +203,17 @@ var _ = Describe("Metal3IPPool controller", func() { expectManager: true, }), Entry("Reconcile normal no cluster", testCaseReconcile{ - m3ipp: &ipamv1.Metal3IPPool{ + m3ipp: &ipamv1.IPPool{ ObjectMeta: testObjectMeta, - Spec: ipamv1.Metal3IPPoolSpec{ClusterName: "abc"}, + Spec: ipamv1.IPPoolSpec{ClusterName: "abc"}, }, reconcileNormal: false, expectManager: false, }), Entry("Reconcile normal no error", testCaseReconcile{ - m3ipp: &ipamv1.Metal3IPPool{ + m3ipp: &ipamv1.IPPool{ ObjectMeta: testObjectMeta, - Spec: ipamv1.Metal3IPPoolSpec{ClusterName: "abc"}, + Spec: ipamv1.IPPoolSpec{ClusterName: "abc"}, }, cluster: &capi.Cluster{ ObjectMeta: testObjectMeta, @@ -235,7 +235,7 @@ var _ = Describe("Metal3IPPool controller", func() { c := fake.NewFakeClientWithScheme(setupScheme()) - ipPoolReconcile := &Metal3IPPoolReconciler{ + ipPoolReconcile := &IPPoolReconciler{ Client: c, ManagerFactory: ipam.NewManagerFactory(c), Log: klogr.New(), @@ -288,7 +288,7 @@ var _ = Describe("Metal3IPPool controller", func() { c := fake.NewFakeClientWithScheme(setupScheme()) - ipPoolReconcile := &Metal3IPPoolReconciler{ + ipPoolReconcile := &IPPoolReconciler{ Client: c, ManagerFactory: ipam.NewManagerFactory(c), Log: klogr.New(), @@ -336,17 +336,17 @@ var _ = Describe("Metal3IPPool controller", func() { ) type TestCaseM3IPCToM3IPP struct { - IPClaim *ipamv1.Metal3IPClaim + IPClaim *ipamv1.IPClaim ExpectRequest bool } - DescribeTable("Metal3IPClaim To Metal3IPPool tests", + DescribeTable("IPClaim To IPPool tests", func(tc TestCaseM3IPCToM3IPP) { - r := Metal3IPPoolReconciler{} + r := IPPoolReconciler{} obj := handler.MapObject{ Object: tc.IPClaim, } - reqs := r.Metal3IPClaimToMetal3IPPool(obj) + reqs := r.IPClaimToIPPool(obj) if tc.ExpectRequest { Expect(len(reqs)).To(Equal(1), "Expected 1 request, found %d", len(reqs)) @@ -367,20 +367,20 @@ var _ = Describe("Metal3IPPool controller", func() { } }, - Entry("No Metal3IPPool in Spec", + Entry("No IPPool in Spec", TestCaseM3IPCToM3IPP{ - IPClaim: &ipamv1.Metal3IPClaim{ + IPClaim: &ipamv1.IPClaim{ ObjectMeta: testObjectMeta, - Spec: ipamv1.Metal3IPClaimSpec{}, + Spec: ipamv1.IPClaimSpec{}, }, ExpectRequest: false, }, ), - Entry("Metal3IPPool in Spec, with namespace", + Entry("IPPool in Spec, with namespace", TestCaseM3IPCToM3IPP{ - IPClaim: &ipamv1.Metal3IPClaim{ + IPClaim: &ipamv1.IPClaim{ ObjectMeta: testObjectMeta, - Spec: ipamv1.Metal3IPClaimSpec{ + Spec: ipamv1.IPClaimSpec{ Pool: corev1.ObjectReference{ Name: "abc", Namespace: "myns", @@ -390,11 +390,11 @@ var _ = Describe("Metal3IPPool controller", func() { ExpectRequest: true, }, ), - Entry("Metal3IPPool in Spec, no namespace", + Entry("IPPool in Spec, no namespace", TestCaseM3IPCToM3IPP{ - IPClaim: &ipamv1.Metal3IPClaim{ + IPClaim: &ipamv1.IPClaim{ ObjectMeta: testObjectMeta, - Spec: ipamv1.Metal3IPClaimSpec{ + Spec: ipamv1.IPClaimSpec{ Pool: corev1.ObjectReference{ Name: "abc", }, diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 104c65da..e38f00a6 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -48,10 +48,6 @@ var k8sClient client.Client var testEnv *envtest.Environment var timestampNow = metav1.Now() -const ( - namespaceName = "testNameSpace" -) - func init() { klog.InitFlags(nil) logf.SetLogger(klogr.New()) @@ -116,21 +112,3 @@ var _ = AfterSuite(func() { err := testEnv.Stop() Expect(err).ToNot(HaveOccurred()) }) - -var deletionTimestamp = metav1.Now() - -func contains(haystack []string, needle string) bool { - for _, straw := range haystack { - if straw == needle { - return true - } - } - return false -} - -func getKey(objectName string) *client.ObjectKey { - return &client.ObjectKey{ - Name: objectName, - Namespace: namespaceName, - } -} diff --git a/docs/api.md b/docs/api.md index a4da8c03..04057b96 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,924 +1,101 @@ # API and Resource Definitions -This describes a setup where the following Cluster API core components and -Metal3-io components are deployed : +This describes a setup where the IPAM component is deployed. It is agnostic of +the IP version. All examples are given with IPv4 but could be IPv6. -* Cluster API manager -* Cluster API Bootstrap Provider Kubeadm (CABPK) manager -* Cluster API Kubeadm Control Plane manager -* Baremetal Operator (including the Ironic setup) -* Cluster API Provider Metal3 (CAPM3) +## IPPool -## BareMetalHost +An IPPool is an object representing a set of IP addresses pools to be used for +IP address allocations. -The BareMetalHost is an object from -[baremetal-operator](https://github.com/metal3-io/baremetal-operator). Each -CR represents a physical host with BMC credentials, hardware status etc. - -BareMetalHost exposes those different fields that are secret references: - -* **userData** : for a cloud-init user-data in a secret with the key `userData` -* **metaData** : for a cloud-init metadata in a secret with the key `metaData` -* **networkData** : for a cloud-init network data in a secret with the key - `networkData` - -For the metaData, soome values are set by default to maintain compatibility: - -* **uuid**: This is the BareMetalHost UID -* **metal3-namespace**: the name of the BareMetalHost -* **metal3-name**: The name of the BareMetalHost -* **local-hostname**: The name of the BareMetalHost -* **local_hostname**: The namespace of the BareMetalHost - -However, setting any of those values in the metaData secret will override those -default values. - -## Cluster - -A Cluster is a Cluster API core object representing a Kubernetes cluster. - -Example cluster: +Example pool: ```yaml -apiVersion: cluster.x-k8s.io/v1alpha3 -kind: Cluster +apiVersion: ipam.metal3.io/v1alpha1 +kind: IPPool metadata: - name: cluster + name: pool1 spec: - clusterNetwork: - services: - cidrBlocks: ["10.96.0.0/12"] - pods: - cidrBlocks: ["192.168.0.0/18"] - serviceDomain: "cluster.local" - infrastructureRef: - apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 - kind: Metal3Cluster - name: m3cluster - controlPlaneRef: - kind: KubeadmControlPlane - apiVersion: controlplane.cluster.x-k8s.io/v1alpha3 - name: m3cluster-controlplane + clusterName: cluster1 + pools: + - start: 192.168.0.10 + end: 192.168.0.30 + prefix: 25 + gateway: 192.168.0.1 + - subnet: 192.168.1.1/26 + - subnet: 192.168.1.128/25 + prefix: 24 + gateway: 192.168.1.1 + preAllocations: + claim1: 192.168.0.12 ``` -## Metal3Cluster +The *spec* field contains the following : -The metal3Cluster object contains information related to the deployment of -the cluster on Baremetal. It currently has two specification fields : +* **clusterName**: That is the name of the cluster to which this pool belongs + it is used to verify whether the resource is paused. +* **pools**: this is a list of IP address pools +* **prefix**: This is a default prefix for this IPPool +* **gateway**: This is a default gateway for this IPPool -* **controlPlaneEndpoint**: contains the target cluster API server address and - port -* **noCloudProvider**: (true/false) Whether the cluster will not be deployed - with an external cloud provider. If set to true, CAPM3 will patch the target - cluster node objects to add a providerID. This will allow the CAPI process to - continue even if the cluster is deployed without cloud provider. +The *prefix* and *gateway* can be overridden per pool. The pool definition is +as follows : -Example metal3cluster : - -```yaml -apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 -kind: Metal3Cluster -metadata: - name: m3cluster -spec: - controlPlaneEndpoint: - host: 192.168.111.249 - port: 6443 - noCloudProvider: true -``` +* **start**: the IP range start address. Can be omitted if **subnet** is set. +* **end**: the IP range end address. Can be omitted. +* **subnet**: the subnet for the allocation. Can be omitted if **start** is set. + It is used to verify that the allocated address belongs to this subnet. +* **prefix**: override of the default prefix for this pool +* **gateway**: override of the default gateway for this pool -## KubeadmControlPlane +## IPClaim -This object contains all information related to the control plane configuration. -It references an **infrastructureTemplate** that must be a -*Metal3MachineTemplate* in this case. +An IPClaim is an object representing a request for an IP address allocation. -For example: +Example pool: ```yaml -kind: KubeadmControlPlane -apiVersion: controlplane.cluster.x-k8s.io/v1alpha3 +apiVersion: ipam.metal3.io/v1alpha1 +kind: IPClaim metadata: - name: m3cluster-controlplane + name: claim1 spec: - replicas: 3 - version: v1.17.0 - infrastructureTemplate: - kind: Metal3MachineTemplate - apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 - name: m3cluster-controlplane - kubeadmConfigSpec: - initConfiguration: - nodeRegistration: - name: 'host0' - kubeletExtraArgs: - cloud-provider: baremetal - clusterConfiguration: - apiServer: - extraArgs: - cloud-provider: baremetal - controllerManager: - extraArgs: - cloud-provider: baremetal - joinConfiguration: - controlPlane: {} - nodeRegistration: - name: 'host0' - kubeletExtraArgs: - cloud-provider: baremetal + pool: + Name: pool1 ``` -## KubeadmConfig - -The KubeadmConfig object is for CABPK. It contains the node Kubeadm -configuration and additional commands to run on the node for the setup. - -In order to deploy Kubernetes successfully, you need to know the cluster API -address before deployment. However, if you are deploying an HA cluster or if you -are deploying without using static ip addresses, the cluster API server address -is unknown. A solution to go around the problem is to deploy Keepalived. -Keepalived allows you to set up a virtual IP, defined beforehand, and shared -by the nodes. Hence the commands to set up Keepalived have to run before -kubeadm. +The *spec* field contains the following : -The content of a KubeadmConfig can contain Jinja2 template elements, since the -cloud-init renders the cloud-config as a Jinja2 template. It is possible to -use metadata from cloud-init, using the following: `{{ ds.meta_data.}}`. -The keys and values are passed to cloud-init through a `Metal3DataTemplate` -object (see below). - -Example KubeadmConfig: - -```yaml -apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3 -kind: KubeadmConfig -metadata: - name: controlplane-0 -spec: - initConfiguration: - nodeRegistration: - name: '{{ ds.meta_data.name }}' - kubeletExtraArgs: - node-labels: 'metal3.io/uuid={{ ds.meta_data.uuid }}' - preKubeadmCommands: - - apt update -y - - apt install net-tools -y - - apt install -y gcc linux-headers-$(uname -r) - - apt install -y keepalived - - systemctl start keepalived - - systemctl enable keepalived - - >- - apt install apt-transport-https ca-certificates curl gnupg-agent - software-properties-common -y - - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - - - >- - add-apt-repository "deb [arch=amd64] - https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" - - apt update -y - - apt install docker-ce docker-ce-cli containerd.io -y - - usermod -aG docker ubuntu - - >- - curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg - | apt-key add - - - >- - echo 'deb https://apt.kubernetes.io/ kubernetes-xenial main' - > /etc/apt/sources.list.d/kubernetes.list - - apt update - - apt install -y kubelet kubeadm kubectl - - systemctl enable --now kubelet - postKubeadmCommands: - - mkdir -p /home/ubuntu/.kube - - cp /etc/kubernetes/admin.conf /home/ubuntu/.kube/config - - chown ubuntu:ubuntu /home/ubuntu/.kube/config - files: - - path: /etc/keepalived/keepalived.conf - content: | - ! Configuration File for keepalived - global_defs { - notification_email { - sysadmin@example.com - support@example.com - } - notification_email_from lb@example.com - smtp_server localhost - smtp_connect_timeout 30 - } - vrrp_instance VI_1 { - state MASTER - interface enp2s0 - virtual_router_id 1 - priority 101 - advert_int 1 - virtual_ipaddress { - 192.168.111.249 - } - } -``` +* **pool**: a reference to the IPPool this request is for -## Machine +## IPAddress -A Machine is a Cluster API core object representing a Kubernetes node. A machine -has a reference to a KubeadmConfig and a reference to a metal3machine. +An IPAddress is an object representing an IP address allocation. -Example Machine: +Example pool: ```yaml -apiVersion: cluster.x-k8s.io/v1alpha3 -kind: Machine +apiVersion: ipam.metal3.io/v1alpha1 +kind: IPAddress metadata: - name: controlplane-0 - labels: - cluster.x-k8s.io/control-plane: "true" - cluster.x-k8s.io/cluster-name: "cluster" + name: pool1-192-168-0-13 spec: - version: 1.16 - bootstrap: - configRef: - apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3 - kind: KubeadmConfig - name: controlplane-0 - infrastructureRef: - apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 - kind: Metal3Machine - name: controlplane-0 + pool: + Name: pool1 + claim: + Name: claim1 + address: 192.168.0.13 + prefix: 24 + gateway: 192.168.0.1 ``` -## Metal3Machine - -The Metal3Machine contains information related to the deployment of the -BareMetalHost such as the image and the host selector. For each machine, there -must be a Metal3Machine. - -The fields are : - -* **image** -- This includes two sub-fields, `url` and `checksum`, which - include the URL to the image and the URL to a checksum for that image. These - fields are required. The image will be used for provisioning of the - `BareMetalHost` chosen by the `Machine` actuator. - -* **userData** -- This includes two sub-fields, `name` and `namespace`, which - reference a `Secret` that contains base64 encoded user-data to be written to - a config drive on the provisioned `BareMetalHost`. This field is optional and - is automatically set by CAPM3 with the userData from the machine object. If - you want to overwrite the userData, this should be done in the CAPI machine. - -* **dataTemplate** -- This includes a reference to a Metal3DataTemplate object - containing the metadata and network data templates, and includes two fields, - `name` and `namespace`. - -* **metaData** is a reference to a secret containing the metadata rendered from - the Metal3DataTemplate metadata template object automatically. In case this - would not be managed by the Metal3DataTemplate controller, if provided by the - user for example, the ownerreference should be set properly to ensure that the - secret belongs to the cluster ownerReference tree (see - [doc](https://cluster-api.sigs.k8s.io/clusterctl/provider-contract.html#ownerreferences-chain)). - -* **networkData** is a reference to a secret containing the network data - rendered from the Metal3DataTemplate metadata template object automatically. - In case this would not be managed by the Metal3DataTemplate controller, if - provided by the user for example, the ownerreference should be set properly to - ensure that the secret belongs to the cluster ownerReference tree (see - [doc](https://cluster-api.sigs.k8s.io/clusterctl/provider-contract.html#ownerreferences-chain)). - The content of the secret should be a yaml equivalent of a json object that - follows the format definition that can be found - [here](https://docs.openstack.org/nova/latest/_downloads/9119ca7ac90aa2990e762c08baea3a36/network_data.json). - -* **hostSelector** -- Specify criteria for matching labels on `BareMetalHost` - objects. This can be used to limit the set of available `BareMetalHost` - objects chosen for this `Machine`. - -The `metaData` and `networkData` field in the `spec` section are for the user -to give directly a secret to use as metaData or networkData. The `userData`, -`metaData` and `networkData` fields in the `status` section are for the -controller to store the reference to the secret that is actually being used, -whether it is from one of the spec fields, or somehow generated. This is aimed -at making a clear difference between the desired state from the user (whether -it is with a DataTemplate reference, or direct `metaData` or `userData` secrets) -and what the controller is actually using. - -The `dataTemplate` field consists of an object reference to a -Metal3DataTemplate object containing the templates for the metadata and -network data generation for this Metal3Machine. The `renderedData` field is a -reference to the Metal3Data object created for this machine. If the -dataTemplate field is set but either the `renderedData`, `metaData` or -`networkData` fields in the status are unset, then the Metal3Machine -controller will wait until it can find the Metal3Data object and the rendered -secrets. It will then populate those fields. - -When CAPM3 controller will set the different fields in the BareMetalHost, -it will reference the metadata secret and the network data secret -in the BareMetalHost. If any of the `metaData` or `networkData` status fields -are unset, that field will also remain unset on the BareMetalHost. - -When the Metal3Machine gets deleted, the CAPM3 controller will remove its -ownerreference from the data template object. This will trigger the deletion of -the generated Metal3Data object and the secrets generated for this machine. - -### hostSelector Examples - -The `hostSelector field has two possible optional sub-fields: - -* **matchLabels** -- Key/value pairs of labels that must match exactly. - -* **matchExpressions** -- A set of expressions that must evaluate to true for - the labels on a `BareMetalHost`. - -Valid operators include: - -* **!** -- Key does not exist. Values ignored. -* **=** -- Key equals specified value. There must only be one - value specified. -* **==** -- Key equals specified value. There must only be one - value specified. -* **in** -- Value is a member of a set of possible values -* **!=** -- Key does not equal the specified value. There must - only be one value specified. -* **notin** -- Value not a member of the specified set of values. -* **exists** -- Key exists. Values ignored. -* **gt** -- Value is greater than the one specified. Value must be - an integer. -* **lt** -- Value is less than the one specified. Value must be - an integer. - -Example 1: Only consider a `BareMetalHost` with label `key1` set to `value1`. - -```yaml -spec: - providerSpec: - value: - hostSelector: - matchLabels: - key1: value1 -``` - -Example 2: Only consider `BareMetalHost` with both `key1` set to `value1` AND -`key2` set to `value2`. - -```yaml -spec: - providerSpec: - value: - hostSelector: - matchLabels: - key1: value1 - key2: value2 -``` - -Example 3: Only consider `BareMetalHost` with `key3` set to either `a`, `b`, or -`c`. - -```yaml -spec: - providerSpec: - value: - hostSelector: - matchExpressions: - - key: key3 - operator: in - values: [‘a’, ‘b’, ‘c’] -``` - -Example 3: Only consider `BareMetalHost` with `key1` set to `value1` AND `key2` -set to `value2` AND `key3` set to either `a`, `b`, or `c`. - -```yaml -spec: - providerSpec: - value: - hostSelector: - matchLabels: - key1: value1 - key2: value2 - matchExpressions: - - key: key3 - operator: in - values: [‘a’, ‘b’, ‘c’] -``` - -### Metal3Machine example - -```yaml -apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 -kind: Metal3Machine -metadata: - name: controlplane-0 -spec: - image: - url: https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img - checksum: https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img.md5sum - hostSelector: - matchLabels: - key1: value1 - matchExpressions: - key: key2 - operator: in - values: {‘abc’, ‘123’, ‘value2’} - dataTemplate: - Name: controlplane-metadata - metaData: - Name: controlplane-0-metadata-0 -``` - -## MachineDeployment - -MachineDeployment is a core Cluster API object that is similar to -deployment for pods. It refers to a KubeadmConfigTemplate and to a -Metal3MachineTemplate. - -Example MachineDeployment: - -```yaml -apiVersion: cluster.x-k8s.io/v1alpha3 -kind: MachineDeployment -metadata: - name: md-0 - labels: - cluster.x-k8s.io/cluster-name: cluster - nodepool: nodepool-0 -spec: - replicas: 1 - selector: - matchLabels: - cluster.x-k8s.io/cluster-name: cluster - nodepool: nodepool-0 - template: - metadata: - labels: - cluster.x-k8s.io/cluster-name: cluster - nodepool: nodepool-0 - spec: - version: 1.16 - bootstrap: - configRef: - name: md-0 - apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3 - kind: KubeadmConfigTemplate - infrastructureRef: - name: md-0 - apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 - kind: Metal3MachineTemplate -``` - -## KubeadmConfigTemplate - -This contains a template to generate KubeadmConfig. - -Example KubeadmConfigTemplate: - -```yaml -apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3 -kind: KubeadmConfigTemplate -metadata: - name: md-0 -spec: - template: - spec: - joinConfiguration: - nodeRegistration: - name: '{{ ds.meta_data.name }}' - kubeletExtraArgs: - node-labels: 'metal3.io/uuid={{ ds.meta_data.uuid }}' - preKubeadmCommands: - - apt update -y - - >- - apt install apt-transport-https ca-certificates curl gnupg-agent - software-properties-common -y - - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - - - >- - add-apt-repository "deb [arch=amd64] - https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" - - apt update -y - - apt install docker-ce docker-ce-cli containerd.io -y - - usermod -aG docker ubuntu - - >- - curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg - | apt-key add - - - >- - echo 'deb https://apt.kubernetes.io/ kubernetes-xenial main' - > /etc/apt/sources.list.d/kubernetes.list - - apt update - - apt install -y kubelet kubeadm kubectl - - systemctl enable --now kubelet -``` - -## Metal3MachineTemplate - -The Metal3MachineTemplate contains the template to create Metal3Machine. - -Example Metal3MachineTemplate : - -```yaml -apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 -kind: Metal3MachineTemplate -metadata: - name: md-0 -spec: - template: - spec: - image: - url: https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img - checksum: https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img.md5sum - hostSelector: - matchLabels: - key1: value1 - matchExpressions: - key: key2 - operator: in - values: {‘abc’, ‘123’, ‘value2’} - dataTemplate: - Name: md-0-metadata -``` - -## Metal3DataTemplate - -```yaml -apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 -kind: Metal3DataTemplate -metadata: - name: nodepool-1 - namespace: default - ownerReferences: - - apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 - controller: true - kind: Metal3Cluster - name: cluster-1 -spec: - metaData: - strings: - - key: abc - value: def - objectNames: - - key: name_m3m - object: metal3machine - - key: name_machine - object: machine - - key: name_bmh - object: baremetalhost - indexes: - - key: index - offset: 0 - step: 1 - ipAddesses: - - key: ip - start: 192.168.0.10 - end: 192.168.0.100 - subnet: 192.168.0.0/24 - step: 1 - fromHostInterfaces: - - key: mac - interface: "eth0" - fromLabels: - - key: label-1 - object: machine - label: mylabelkey - fromAnnotations: - - key: annotation-1 - object: machine - annotation: myannotationkey - networkData: - links: - ethernets: - - type: "phy" - id: "enp1s0" - mtu: 1500 - macAddress: - fromHostInterface: "eth0" - - type: "phy" - id: "enp2s0" - mtu: 1500 - macAddress: - fromHostInterface: "eth1" - bonds: - - id: "bond0" - mtu: 1500 - macAddress: - string: "XX:XX:XX:XX:XX:XX" - bondMode: "802.1ad" - bondLinks: - - enp1s0 - - enp2s0 - vlans: - - id: "vlan1" - mtu: 1500 - macAddress: - string: "YY:YY:YY:YY:YY:YY" - vlanId: 1 - vlanLink: bond0 - networks: - ipv4DHCP: - - id: "provisioning" - link: "bond0" - - ipv4: - - id: "Baremetal" - link: "vlan1" - ipAddress: - start: "192.168.0.10" - end: "192.168.0.100" - subnet: "192.168.0.0/24" - step: 1 - netmask: 24 - routes: - - network: "0.0.0.0" - netmask: 0 - gateway: "192.168.0.1" - services: - - type: "dns" - address: "8.8.4.4" - ipv6DHCP: - - id: "provisioning6" - link: "bond0" - ipv6SLAAC: - - id: "provisioning6slaac" - link: "bond0" - ipv6: - - id: "Baremetal6" - link: "vlan1" - ipAddress: - start: "2001:0db8:85a3::8a2e:0370:a" - end: "2001:0db8:85a3::8a2e:0370:fff0" - subnet: "2001:0db8:85a3::8a2e:0370:0/64" - step: 10 - netmask: 64 - routes: - - network: "0::0" - netmask: 0 - gateway: "2001:0db8:85a3::8a2e:0370:1" - services: - - dns: "2001:4860:4860::8844" - services: - dns: - - "8.8.8.8" - - "2001:4860:4860::8888" -status: - indexes: - "0": "machine-1" - dataNames: - "machine-1": nodepool-1-0 - lastUpdated: "2020-04-02T06:36:09Z" -``` - -This object will be reconciled by its own controller. When reconciled, -the controller will add a label pointing to the Metal3Cluster that has nodes -linking to this object. The spec contains a `metaData` and a `networkData` field -that contain a template of the values that will be rendered for all nodes. - -The `metaData` field will be rendered into a map of strings in yaml format, -while `networkData` will be rendered into a map equivalent of -[Nova network_data.json](https://docs.openstack.org/nova/latest/user/metadata.html#openstack-format-metadata). -On the target node, the network data will be rendered as a json object that -follows the format definition that can be found -[here](https://docs.openstack.org/nova/latest/_downloads/9119ca7ac90aa2990e762c08baea3a36/network_data.json). - -### Metadata Specifications - -The `metaData` field contains a list of items that will render data in different -ways. The following types of objects are available and accept lists: - -* **strings**: renders the given string as value in the metadata. It takes a - `value` attribute. -* **objectNames** : renders the name of the object that matches the type given. - It takes an `object` attribute, containing the type of the object. -* **indexes**: renders the index of the current object, with the offset from the - `offset` field and using the step from the `step` field. The following - conditions must be matched : `offset` >= 0 and `step` >= 1 - if the step is unspecified (default value being 0), the controller will - automatically change it for 1. The `prefix` and `suffix` attributes are to - provide a prefix and a suffix for the rendered index. -* **ipAddresses**: renders an ip address based on the index, based on the `start` value - if given or using `subnet` to calculate the start value, and checking that - the rendered value is not over the `end` value. The increment is the `step` - value. If the computed value goes out of bounds, the error status will be set - with the error in the error message. In case of using the `subnet` value to - get the start IP address, it will be the second IP of the subnet (for example - `192.168.0.1` for a subnet `192.168.0.0/24`). -* **fromHostInterfaces**: renders the MAC address of the BareMetalHost that - matches the name given as value. -* **fromLabels**: renders the content of a label on an object or an empty string - if the label is absent. It takes an `object` attribute to specify the type of - the object where to fetch the label, and a `label` attribute that contains the - label key. -* **fromAnnotations**: renders the content of a annotation on an object or an - empty string if the annotation is absent. It takes an `object` attribute to - specify the type of the object where to fetch the annotation, and an - `annotation` attribute that contains the annotation key. - -For each object, the attribute **key** is required. - -### networkData specifications - -The `networkData` field will contain three items : - -* **links**: a list of layer 2 interface -* **networks**: a list of layer 3 networks -* **services** : a list of services (DNS) - -#### Links specifications - -The object for the **links** section list can be: - -* **ethernets**: a list of ethernet interfaces -* **bonds**: a list of bond interfaces -* **vlans**: a list of vlan interfaces - -The **links/ethernets** objects contain the following: - -* **type**: Type of the ethernet interface -* **id**: Interface name -* **mtu**: Interface MTU -* **macAddress**: an object to render the MAC Address - -The **links/ethernets/type** can be one of : - -* bridge -* dvs -* hw_veb -* hyperv -* ovs -* tap -* vhostuser -* vif -* phy - -The **links/ethernets/macAddress** object can be one of: - -* **string**: with the desired Mac given as a string -* **fromHostInterface**: with the interface name from BareMetalHost - hardware details. - -The **links/bonds** object contains the following: - -* **id**: Interface name -* **mtu**: Interface MTU -* **macAddress**: an object to render the MAC Address -* **bondMode**: The bond mode -* **bondLinks** : a list of links to use for the bond - -The **links/bonds/bondMode** can be one of : - -* 802.1ad -* balance-rr -* active-backup -* balance-xor -* broadcast -* balance-tlb -* balance-alb - -The **links/vlans** object contains the following: - -* **id**: Interface name -* **mtu**: Interface MTU -* **macAddress**: an object to render the MAC Address -* **vlanId**: The vlan ID -* **vlanLink** : The link on which to create the vlan - -#### The networks specifications - -The object for the **networks** section can be: - -* **ipv4**: a list of ipv4 static allocations -* **ipv4DHCP**: a list of ipv4 DHCP based allocations -* **ipv6**: a list of ipv6 static allocations -* **ipv6DHCP**: a list of ipv6 DHCP based allocations -* **ipv6SLAAC**: a list of ipv6 SLAAC based allocations - -The **networks/ipv4** object contains the following: - -* **id**: the network name -* **link**: The name of the link to configure this network for -* **ipAddress**: the IP address object -* **netmask**: the netmask, in an integer format -* **routes**: the list of route objects - -The **networks/ipv4/ipAddress** is an address object containing: - -* **start**: the start IP address -* **end**: The end IP address -* **subnet**: The subnet in a CIDR notation "X.X.X.X/X" -* **step**: the step between IP addresses - -If the **subnet** is specified, then **start** and **end** are not required and -reverse, if **start** and **end** are specified, then **subnet** is not required - -The **networks/ipv4/routes** is a route object containing: - -* **network**: the subnet to reach -* **netmask**: the mask of the subnet as integer -* **gateway**: the gateway to use -* **services**: a list of services object as defined later - -The **networks/ipv4Dhcp** object contains the following: - -* **id**: the network name -* **link**: The name of the link to configure this network for -* **routes**: the list of route objects - -The **networks/ipv6** object contains the following: - -* **id**: the network name -* **link**: The name of the link to configure this network for -* **ipAddress**: the IP address object -* **netmask**: the netmask, in an integer format -* **routes**: the list of route objects - -The **networks/ipv6Dhcp** object contains the following: - -* **id**: the network name -* **link**: The name of the link to configure this network for -* **routes**: the list of route objects - -The **networks/ipv6Slaac** object contains the following: - -* **id**: the network name -* **link**: The name of the link to configure this network for -* **routes**: the list of route objects - -#### the services specifications - -The object for the **services** section can be: - -* **dns**: a list of dns service with the ip address of a dns server - -## The Metal3Data object - -The output of the controller would be a Metal3Data object,one per node linking to the -Metal3DataTemplate object and the associated secrets - -The Metal3Data object would be: - -```yaml -apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 -kind: Metal3Data -metadata: - name: nodepool-1-0 - namespace: default - ownerReferences: - - apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 - controller: true - kind: Metal3DataTemplate - name: nodepool-1 -spec: - index: 0 - metaData: - name: machine-1-metadata - namespace: default - networkData: - name: machine-1-metadata - namespace: default - metal3Machine: - name: machine-1 - namespace: default -status: - ready: true - error: false - errorMessage: "" -``` - -The Metal3Data will contain the index of this node, and links to the secrets -generated and to the Metal3Machine using this Metal3Data object. - -If the Metal3DataTemplate object is updated, the generated secrets will not be -updated, to allow for re-provisioning of the nodes in the exact same state as -they were initially provisioned. Hence, to do an update, it is necessary to do -a rolling upgrade of all nodes. - -The reconciliation of the Metal3DataTemplate object will also be triggered by -changes on Metal3Machines. In the case that a Metal3Machine gets modified, if -the `dataTemplate` references a Metal3DataTemplate, that object will be reconciled. -There will be two cases: - -* An already generated Metal3Data object exists with an ownerReference to this - Metal3Machine. In that case, the reconciler will verify that the required - secrets exist. If they do not, they will be created. -* if no Metal3Data exists with an ownerReference to this Metal3Machine, then the - reconciler will create one and fill the respective field with the secret name. - -To create a Metal3Data object, the Metal3DataTemplate controller will select an -index for that Metal3Machine. The selection happens by selecting the lowest -available index that is not in the `indexes` field of the status. If the -`indexes` field is empty, the controller will list all existing Metal3Data -object linked to this Metal3DataTemplate and recreate the unavailable indexes. -It will fill it by extracting the index from the Metal3Data names. The indexes -always start from 0 and increment by 1. The lowest available index is to be used -next. The `dataNames` field contains the map of Metal3Machine to Metal3Data. - -Once the next lowest available index is found, it will create the Metal3Data -object. The name would be a concatenation of the Metal3DataTemplate name and -index. Upon conflict, it will fetch again the list to consider the new list of -Metal3Data and try to create the new object with the new index, this will happen -until the new object is created successfully. Upon success, it will render the -content values, and create the secrets containing the rendered data. The -controller will generate the content based on the `metaData` or `networkData` -field of the Metal3DataTemplate Specs. - -Once the generation is successful, the status field `ready` will be set to True. -If any error happens during the rendering, an error message will be added. - -## The generated secrets +The *spec* field contains the following : -The name of the secret will be made of a prefix and the index. The Metal3Machine -object name will be used as the prefix. A `-metadata-` or `-networkdata-` will -be added between the prefix and the index. +* **pool**: a reference to the IPPool this address is for +* **claim**: a reference to the IPClaim this address is for +* **address**: the allocated IP address +* **prefix**: the prefix for this address +* **gateway**: the gateway for this address ## Metal3 dev env examples diff --git a/docs/architecture.md b/docs/architecture.md deleted file mode 100644 index 74a69bb2..00000000 --- a/docs/architecture.md +++ /dev/null @@ -1,459 +0,0 @@ -# Architecture - -## Introduction - -The ```cluster-api-provider-metal3 (CAPM3)``` is one of the controllers -involved in managing the life cycle of kubernetes clusters on Metal3 -Machines. This document describes the components involved and their roles. It -also discusses the flow of information from one CR to another with the help of -the controllers. As to avoid ambiguity, we will refer to the physical or virtual -machines managed by the controllers as Bare Metal Servers. And, the kubernetes -resources (CRs) representing them as Metal3 Machines (M3M). - -## Components - -The ```cluster-api-provider-metal3 (CAPM3)``` operator is responsible for -watching and reconciling multiple resources. However, it is important to see -other controllers and custom resources (CRs) involved in the process. The -ultimate goal of the interaction between the controllers and CRs is to provision -a kubernetes cluster on Bare Metal Servers. To that end, the controllers -perform different actions on one or more relevant CRs. - -The following diagram shows the different controllers and CRs involved. The -CAPI, CABPK and CAPM3 controllers are beyond the scope of this document. -With respect to CAPM3, we focus on what CRS it `watches` and `reconciles`. The -arrows in black show which CRs the controller `reconciles` while the one in red -show that a related controller is `watching` another CR. - -![components](images/components.png) - -As shown in the above Components' diagram, CAPM3 is watching for changes in -`Machine` CR and upon change, it makes changes on the `Metal3Machine` CR. -Similarly, it watches `Cluster` CR and makes changes on a related -`Metal3Cluster` CR. - -The left most components, BMO controller and BareMetalHost(BMH) CR, are the -closest to the Bare Metal Server. If one wants to changes the state of a Metal3 -Machine, they modify the BMH CR. Upon change to the BMH, BMO interacts -with Ironic to make changes on the Bare Metal Server. - -During the initial introspection and state changes, the above logic works in the - opposite direction as well. Information gathered during introspection or any - state changes on the Bare Metal Server, results in BMO learning about the - change(s) via ironic and a chain of events starts. Once BMO learns about the - changes, it makes the required -changes on the BMH. - -As discussed above, the management of Bare Metal Servers requires the -interaction of multiple controllers via multiple CRs. However, the interaction -is performed on a specified number of fields on each CR. i.e. A controller -`watches` a specified number of fields in each CR and makes changes on a set of - fields. Before seeing the relationship, we need to see the CRs at two - stages, before and after provisioning a control plane machine. - ---- - -### Cluster - -Cluster, User provided Configuration - -```yaml -apiVersion: cluster.x-k8s.io/v1alpha3 -kind: Cluster -metadata: - name: test1 -spec: - clusterNetwork: - services: - cidrBlocks: ["10.96.0.0/12"] - pods: - cidrBlocks: ["192.168.0.0/18"] - serviceDomain: "cluster.local" - infrastructureRef: - apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 - kind: Metal3Cluster - name: test1 -``` - -Cluster, after reconciliation - -```yaml -apiVersion: cluster.x-k8s.io/v1alpha3 -kind: Cluster -metadata: - name: test1 - namespace: metal3 -spec: - clusterNetwork: - pods: - cidrBlocks: - - 192.168.0.0/18 - serviceDomain: cluster.local - services: - cidrBlocks: - - 10.96.0.0/12 - |----------------------------------------------------------------------------| - |# infrastructureRef comes from 'Metal3Cluster' and is added by 'CAPM3' | - | infrastructureRef: | - | apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 | - | kind: Metal3Cluster | - | name: test1 | - |----------------------------------------------------------------------------- -status: - apiEndpoints: - - host: 192.168.111.249 - port: 6443 - controlPlaneInitialized: true - infrastructureReady: true - phase: provisioned -``` - ---- - -### Metal3Cluster - -Metal3Cluster, User provided Configuration for - -```yaml -apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 -kind: Metal3Cluster -metadata: - name: test1 -spec: - apiEndpoint: https://192.168.111.249:6443 - noCloudProvider: true -``` - -Metal3Cluster, after reconciliation - -```yaml -apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 -kind: Metal3Cluster -metadata: - name: test1 - namespace: metal3 - |----------------------------------------------------------------------------| - |# ownerReferences refers to the linked Cluster and is added by 'CAPM3' | - |ownerReferences: | - |- apiVersion: cluster.x-k8s.io/v1alpha3 | - | kind: Cluster | - | name: test1 | - | uid: 193ec580-89db-46cd-b6f7-ddc0cd79636d | - |----------------------------------------------------------------------------| -spec: - apiEndpoint: https://192.168.111.249:6443 - noCloudProvider: true -status: - apiEndpoints: - - host: 192.168.111.249 - port: 6443 - ready: true -``` - ---- - -### Machine - -Machine, User provided Configuration - -```yaml -apiVersion: cluster.x-k8s.io/v1alpha3 -kind: Machine -metadata: - name: test1-controlplane-0 - labels: - cluster.x-k8s.io/control-plane: "true" - cluster.x-k8s.io/cluster-name: "test1" -spec: - version: v1.17.0 - bootstrap: - configRef: - apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3 - kind: KubeadmConfig - name: test1-controlplane-0 - infrastructureRef: - apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 - kind: Metal3Machine - name: test1-controlplane-0 -``` - -Machine, after reconciliation - -```yaml -apiVersion: cluster.x-k8s.io/v1alpha3 -kind: Machine -metadata: - labels: - cluster.x-k8s.io/cluster-name: test1 - cluster.x-k8s.io/control-plane: "true" - name: test1-controlplane-0 - namespace: metal3 - ownerReferences: - - apiVersion: cluster.x-k8s.io/v1alpha3 - kind: Cluster - name: test1 - uid: 193ec580-89db-46cd-b6f7-ddc0cd79636d -spec: - bootstrap: - configRef: - apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3 - kind: KubeadmConfig - name: test1-controlplane-0 - |----------------------------------------------------------------------------| - |# data comes from 'KubeadmConfig.status.bootstrapData' & is added by 'CAPI' | - | data: | - |----------------------------------------------------------------------------| - infrastructureRef: - apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 - kind: Metal3Machine - name: test1-controlplane-0 - providerID: metal3://8e16d3b6-d48c-41e0-af0f-e43dbf5ec0cd - version: v1.17.0 -status: - addresses: - - address: 172.22.0.10 - type: InternalIP - - address: 192.168.111.21 - type: InternalIP - - address: node-1 - type: Hostname - - address: node-1 - type: InternalDNS - bootstrapReady: true - infrastructureReady: true - nodeRef: - name: node-1 - phase: running -``` - ---- -### Metal3Machine - -Metal3Machine, User provided Configuration - -```yaml -apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 -kind: Metal3Machine -metadata: - name: test1-controlplane-0 -spec: - image: - url: http://172.22.0.1/images/bionic-server-cloudimg-amd64.img - checksum: http://172.22.0.1/images/bionic-server-cloudimg-amd64.img.md5sum -``` - -Metal3Machine, after reconciliation - -```yaml -apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 -kind: Metal3Machine -metadata: - name: test1-controlplane-0 - namespace: metal3 - # ownerReferences refers to the linked Machine and is added by 'CAPM3' - ownerReferences: - - apiVersion: cluster.x-k8s.io/v1alpha3 - kind: Machine - name: test1-controlplane-0 -spec: - hostSelector: {} - image: - checksum: http://172.22.0.1/images/bionic-server-cloudimg-amd64.img.md5sum - url: http://172.22.0.1/images/bionic-server-cloudimg-amd64.img - providerID: metal3://8e16d3b6-d48c-41e0-af0f-e43dbf5ec0cd - |----------------------------------------------------------------------------| - |# userData comes from 'Machine' and is added by 'CAPM3' | - |userData: | - | name: test1-controlplane-0-user-data | - | namespace: metal3 | - |----------------------------------------------------------------------------| -status: - addresses: - - address: 172.22.0.10 - type: InternalIP - - address: 192.168.111.21 - type: InternalIP - - address: node-1 - type: Hostname - - address: node-1 - type: InternalDNS - ready: true -``` - ---- - -### BareMetalHost - -BareMetalHost, User provided Configuration - -```yaml -apiVersion: metal3.io/v1alpha1 -kind: BareMetalHost -metadata: - name: node-1 -spec: - online: true - bootMACAddress: 00:b2:8c:ee:22:98 - bmc: - address: ipmi://192.168.111.1:6231 - credentialsName: node-1-bmc-secret -``` - -BareMetalHost, after reconciliation - -```yaml -apiVersion: metal3.io/v1alpha1 -kind: BareMetalHost -metadata: - name: node-1 - namespace: metal3 -spec: - bmc: - address: ipmi://192.168.111.1:6231 - credentialsName: node-1-bmc-secret - bootMACAddress: 00:b2:8c:ee:22:98 - |----------------------------------------------------------------------------| - |# consumerRef refers to the linked Metal3Machine is added by 'CAPM3' | - |consumerRef: | - | apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 | - | kind: Metal3Machine | - | name: test1-controlplane-0 | - | namespace: metal3 | - |# Image comes from 'Metal3Machine' and is added by 'CAPM3' | - |image: | - | checksum: http://172.22.0.1/images/bionic-server-cloudimg-amd64.img.md5sum| - | url: http://172.22.0.1/images/bionic-server-cloudimg-amd64.img | - |online: true | - |# UserData comes from 'Metal3Machine' and is added by 'CAPBK' | - |userData: | - | name: test1-controlplane-0-user-data | - | namespace: metal3 | - |----------------------------------------------------------------------------| -status: - goodCredentials: - credentials: - name: node-1-bmc-secret - namespace: metal3 - hardware: - hardwareProfile: unknown - - operationHistory: .... - operationalStatus: OK - poweredOn: true - provisioning: - ID: 75450b93-0476-45b7-8629-d1eebbdc558b - image: - checksum: http://172.22.0.1/images/bionic-server-cloudimg-amd64.img.md5sum - url: http://172.22.0.1/images/bionic-server-cloudimg-amd64.img - state: provisioned - triedCredentials: - credentials: - name: node-1-bmc-secret - namespace: metal3 - credentialsVersion: "1435" -``` - -### KubeadmConfig - -KubeadmConfig, user provided Configuration - -```yaml -apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3 -kind: KubeadmConfig -metadata: - name: test1-controlplane-0 -spec: - initConfiguration: - nodeRegistration: - name: '{{ ds.meta_data.name }}' - kubeletExtraArgs: - node-labels: 'metal3.io/uuid={{ ds.meta_data.uuid }}' - preKubeadmCommands: - postKubeadmCommands: - files: -``` - -KubeadmConfig, after reconciliation - -```yaml -apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3 -kind: KubeadmConfig -metadata: - name: test1-controlplane-0 - namespace: metal3 - ownerReferences: - - apiVersion: cluster.x-k8s.io/v1alpha3 - kind: Machine - name: test1-controlplane-0 -spec: - clusterConfiguration: - apiServer: {} - apiVersion: kubeadm.k8s.io/v1beta1 - certificatesDir: /etc/kubernetes/pki - clusterName: test1 - controlPlaneEndpoint: 192.168.111.249:6443 - controllerManager: {} - dns: - type: "" - etcd: {} - imageRepository: "" - kind: ClusterConfiguration - kubernetesVersion: v1.17.0 - networking: - dnsDomain: cluster.local - podSubnet: 192.168.0.0/18 - serviceSubnet: 10.96.0.0/12 - scheduler: {} - files: - initConfiguration: - nodeRegistration: - kubeletExtraArgs: - node-labels: metal3.io/uuid={{ ds.meta_data.uuid }} - name: '{{ ds.meta_data.name }}' - postKubeadmCommands: -status: - |----------------------------------------------------------------------------| - |bootstrapData: | - |----------------------------------------------------------------------------| - ready: true -``` - ---- -#### Flow of information - -As was shown on the above CRs, some of the fields are introduced in one CR and -they travel through multiple CRs to reach the BMH, which is the closest to the -Bare Metal Server. There is also a movement of information from the -virtual/physical machines towards the CRs, but this is beyond the scope of this - document. Some of the fields are added by users, while the others are by the - relevant controllers. - -We have added the source of relevant fields as comments in the above yaml files. -The following sequence diagram shows the flow of information (fields) across -multiple CRs with the help of controllers. - -![field mapping](images/controllerssequencediagram.png) - -#### Some relevant fields - -```apiEndpoint:``` IP:Port of a load balancer (keepalived VIP) - -```image:``` OS image for the Metal3 Machine - -The following fields are used to make a relationship between CRs. - -```bash -infrastructureRef -ownerReferences -consumerRef -``` - -The following fields refer to the cloud-init data, but the names change across -different CRs. - -```bash -bootstrap -bootstrap.data -userData -bootstrapData -``` diff --git a/docs/deployment_workflow.md b/docs/deployment_workflow.md index c82a341a..1cd4b5cb 100644 --- a/docs/deployment_workflow.md +++ b/docs/deployment_workflow.md @@ -5,82 +5,35 @@ The following controllers need to be deployed : * CAPI -* CAPBK or alternative -* CACPK or alternative -* CAPM3 -* Baremetal Operator, with Ironic setup +* IPAM -## Requirements +The IPAM controller has a dependency on Cluster API *Cluster* objects. -The cluster should either : +## Requirements -* be deployed with an external cloud provider, that would be deployed as part of - the userData and would set the providerIDs based on the BareMetalHost UID. -* Be deployed with the ProviderID set to be the BareMetalHostUUID -* be deployed with each node with the label "metal3.io/uuid" set to the - BareMetalHost UID that is provided by ironic to cloud-init through the - metadata `ds.meta_data.uuid`. This can be achieved by setting the following in - the KubeadmConfig : +CAPI CRDs and controllers must be deployed and the cluster objects exist for +successful deployments -```yaml -nodeRegistration: - name: '{{ ds.meta_data.name }}' - kubeletExtraArgs: - node-labels: 'metal3.io/uuid={{ ds.meta_data.uuid }}' -``` +## Deployment -## Deploying the CRs +You can create the **IPPool** object independently. It will wait for its cluster +to exist before reconciling. -You can deploy the CRs all at the same time, the controllers will take care of -following the correct flow. -An outline of the workflow is below. +If you wish to deploy **IPAddress** objects manually, then they should be +deployed before any claims. It is highly recommended to use the *preAllocations* +field itself or have the reconciliation paused (during clusterctl move for +example). -1. The CAPI controller will set the OwnerRef on the Metal3Cluster referenced - by the Cluster, on the KubeadmControlPlane, and all machines, KubeadmConfig - and Metal3Machines created by the user or by a MachineDeployment. -1. The CAPM3 controller will verify the controlPlaneEndpoint field and populate - the status with ready field set to true. -1. The CAPI controller will set infrastructureReady field to true on the Cluster -1. The CAPI controller will set the OwnerRef -1. The KubeadmControlPlane controller will wait until the cluster has - infrastructureReady set to true, and generate the first machine, - Metal3Machine and KubeadmConfig. -1. CABPK will generate the cloud-init output for this machine and create a - secret containing it. -1. The CAPI controller will copy the userData secret name into the machine - object and set the bootstrapReady field to true. -1. Once the secrets exists, the CAPM3 controller will select, if - possible, a BareMetalHost that matches the criteria, or wait until one is - available. -1. Once the machine has been associated with a BaremetalHost, the CAPM3 - controller will check if the Metal3Machine references a - Metal3DataTemplate object. In that case, it will set an OwnerReference on the - Metal3DataTemplate object referencing the Metal3Machine and wait for the - metadata and/or network data secrets to be created. -1. The CAPM3 controller reconciling the Metal3DataTemplate object will select - the lowest available index for the new machine and create a Metal3Data - object that will then create the secrets containing the rendered data. -1. The CAPM3 controller will then set the BareMetalHost spec accordingly to the - Metal3Machine specs. -1. The BareMetal Operator will then start the deployment. -1. After deployment, the BaremetalHost will be in provisioned state. However, - initialization is not complete. If deploying without cloud provider, CAPM3 - can wait until the target cluster is up and the node appears, then fetch - the node by matching the label `metal3.io/uuid=` and set the - providerID to `metal3://`. The Metal3Machine ready status will - be set to true and the providerID will be set to `metal3://` on the - Metal3Machine. -1. CAPI will access the target cluster and compare the providerID on the node to - the providerID of the Machine, copied from the metal3machine. If matching, - the control plane initialized status will be set to true and the machine - state to running. -1. CACPK will then do the same for each further controller node until reaching - the desired replicas number, one by one, triggering the same workflow. - Meanwhile, as soon as the controlplane is initialized, CABPK will generate - the user data for all other machines, triggering their deployment. +After an **IPClaim** object creation, the controller will list all existing +**IPAddress** objects. It will then select randomly an address that has not been +allocated yet and is not in the *preAllocations* map. It will then create an +**IPAddress** object containing the references to the **IPPool** and **IPClaim** +and the address, the prefix from the address pool or the default prefix, and the +gateway from the address pool or the default gateway. ## Deletion -Deleting the cluster object will trigger the deletion of all related objects -except for KubeadmConfigTemplates, Metal3MachineTemplates, Metal3DataTemplates -and BareMetalHosts, and the secrets related to the BareMetalHosts. +When deleting and **IPClaim** object, the controller will simply delete the +associated **IPAddress** object. Once all **IPAddress** objects have been +deleted, the **IPPool** object can be deleted. Before that point, the finalizer +in the **IPPool** object will block the deletion. diff --git a/docs/dev-setup.md b/docs/dev-setup.md deleted file mode 100644 index 488b4ac9..00000000 --- a/docs/dev-setup.md +++ /dev/null @@ -1,75 +0,0 @@ -# Setting up a development environment - -## Pre-requisites - -CAPM3 requires two external tools for running the tests -during development. - -### Install kustomize - -```bash -./hack/tools/install_kustomize.sh -``` - -### Install kubebuilder - -```bash -./hack/tools/install_kubebuilder.sh -``` - -## Development using Kind or Minikube - -See the [Kind docs](https://kind.sigs.k8s.io/docs/user/quick-start) for -instructions on launching a Kind cluster and the -[Minikube docs](https://kubernetes.io/docs/setup/minikube/) for -instructions on launching a Minikube cluster. - -### Add CRDs and CRs from baremetal-operator - -The provider also uses the `BareMetalHost` custom resource that’s defined by -the `baremetal-operator`. The following command deploys the CRD and creates -dummy BareMetalHosts. - -```sh - make deploy-bmo-cr -``` - -When a `Metal3Machine` gets created, the provider looks for an available -`BareMetalHost` to claim and then sets it to be provisioned to fulfill the -request expressed by the `Metal3Machine`. Before creating a -`Metal3Machine`, we can create a dummy `BareMetalHost` object. There’s no -requirement to actually run the -`baremetal-operator` to test the reconciliation logic of the provider. - -Refer to the [baremetal-operator developer -documentation](https://github.com/metal3-io/baremetal-operator/blob/master/docs/dev-setup.md) -for instructions and tools for creating BareMetalHost objects. - -### Deploy CAPI and CAPM3 - -The following command will deploy the controllers from CAPI, CABPK and CAPM3 and -the requested CRDs. - -```sh - make deploy -``` - -### Run the Controller locally - -You will first need to scale down the controller deployment : - -```sh - kubectl scale -n capm3-system deployment.v1.apps/capm3-controller-manager \ - --replicas 0 -``` - -You can manually run the controller from outside of the cluster for development -and testing purposes. There’s a `Makefile` target which makes this easy. - -```bash -make run -``` - -You can follow the output on the console to see information about what the -controller is doing. You can also proceed to create/update/delete -`Metal3Machines` and `BareMetalHosts` to test the controller logic. diff --git a/docs/getting-started.md b/docs/getting-started.md deleted file mode 100644 index 92d176b6..00000000 --- a/docs/getting-started.md +++ /dev/null @@ -1,219 +0,0 @@ -# Cluster API provider Bare Metal - -This provider integrates with the -[Cluster API project](https://github.com/kubernetes-sigs/cluster-api). - -## Setup - -### Pre-requisites - -The pre-requisite for the deployment of CAPM3 are the following: - -- [Baremetal-Operator](https://github.com/metal3-io/baremetal-operator) deployed -- Ironic up and running (inside or outside of the cluster) -- BareMetalHost resources created for all hardware nodes and in "ready" or - "available" state -- If deploying CAPI in a multi-tenancy scenario, all cluster-related CRs (inc. - BareMetalHosts and related) must be in the same namespace. This is due to the - fact that the controllers are restricted to their own namespace with RBAC. - -### Using clusterctl - -Please refer to -[Clusterctl documentation](https://master.cluster-api.sigs.k8s.io/clusterctl/overview.html). -Once the Pre-requisites are fulfilled, you can follow the normal clusterctl -flow for the `init`, `config`, `upgrade` and `delete` workflow. The `move` -command is supported only if the baremetalhosts are moved independently first, -and their status is preserved during the move. Please refer to the *Pivoting -Ironic* section. - -### Cluster templates variables - -You can find an example file containing the environment variables -`example_variables.rc`in the release or -[here](https://github.com/metal3-io/cluster-api-provider-metal3/tree/master/examples/clusterctl-templates/example_variables.rc) - -#### POD_CIDR - -This is the CIDR for the pod. It can be given as a comma separated list of -quoted elements. For example: - -`POD_CIDR='"192.168.0.0/24", "192.168.1.0/24"'` - -#### SERVICE_CIDR - -This is the CIDR for the services. It can be given as a comma separated list of -quoted elements. For example: - -`SERVICE_CIDR='"192.168.2.0/24", "192.168.3.0/24"'` - -#### API_ENDPOINT_HOST - -This is the API endpoint name or IP address. For example: - -`API_ENDPOINT_HOST="192.168.111.249"` - -#### API_ENDPOINT_PORT - -This is the API endpoint port. For example: - -`API_ENDPOINT_PORT="6443"` - -#### IMAGE_URL - -This is the URL of the image to deploy. It should be a qcow2 image. For example: - -`IMAGE_URL="http://192.168.0.1/ubuntu.qcow2"` - -#### IMAGE_CHECKSUM - -This is the URL of the md5sum of the image to deploy. For example: - -`IMAGE_CHECKSUM="http://192.168.0.1/ubuntu.qcow2.md5sum"` - -#### CTLPLANE_KUBEADM_EXTRA_CONFIG - -This contains the extra configuration to pass in KubeadmControlPlane. It is -critical to maintain the indentation. The allowed keys are : - -- preKubeadmCommands -- postKubeadmCommands -- files -- users -- ntp -- format - -Here is an example for Ubuntu: - -```bash -CTLPLANE_KUBEADM_EXTRA_CONFIG=" - preKubeadmCommands: - - ip link set dev enp2s0 up - - dhclient enp2s0 - - apt update -y - - netplan apply - - >- - apt install net-tools gcc linux-headers-$(uname -r) bridge-utils - apt-transport-https ca-certificates curl gnupg-agent - software-properties-common -y - - apt install -y keepalived && systemctl stop keepalived - - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - - - add-apt-repository \"deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\" - - curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - - - echo 'deb https://apt.kubernetes.io/ kubernetes-xenial main' > /etc/apt/sources.list.d/kubernetes.list - - apt update -y - - apt install docker-ce docker-ce-cli containerd.io kubelet kubeadm kubectl -y - - systemctl enable --now docker kubelet - - if (curl -sk --max-time 10 https://{{ CLUSTER_APIENDPOINT_HOST }}:6443/healthz); then echo \"keepalived already running\";else systemctl start keepalived; fi - - usermod -aG docker ubuntu - postKubeadmCommands: - - mkdir -p /home/ubuntu/.kube - - cp /etc/kubernetes/admin.conf /home/ubuntu/.kube/config - - systemctl enable --now keepalived - - chown ubuntu:ubuntu /home/ubuntu/.kube/config - files: - - path: /etc/keepalived/keepalived.conf - content: | - ! Configuration File for keepalived - global_defs { - notification_email { - sysadmin@example.com - support@example.com - } - notification_email_from lb@example.com - smtp_server localhost - smtp_connect_timeout 30 - } - vrrp_instance VI_2 { - state MASTER - interface enp2s0 - virtual_router_id 2 - priority 101 - advert_int 1 - virtual_ipaddress { - {{ CLUSTER_APIENDPOINT_HOST }} - } - } - - path: /etc/netplan/50-cloud-init.yaml - owner: root:root - permissions: '0644' - content: | - network: - ethernets: - enp2s0: - dhcp4: true - version: 2 - - path : /etc/netplan/60-ironicendpoint.yaml - owner: root:root - permissions: '0644' - content: | - network: - version: 2 - renderer: networkd - bridges: - ironicendpoint: - interfaces: [enp1s0] - dhcp4: yes -" -``` - -#### WORKERS_KUBEADM_EXTRA_CONFIG - -This contains the extra configuration to pass in KubeadmConfig for workers. It -is critical to maintain the indentation. The allowed keys are : - -- preKubeadmCommands -- postKubeadmCommands -- files -- users -- ntp -- format - -Here is an example for Ubuntu: - -```bash -WORKERS_KUBEADM_EXTRA_CONFIG=" - preKubeadmCommands: - - ip link set dev enp2s0 up - - dhclient enp2s0 - - apt update -y - - netplan apply - - >- - apt install apt-transport-https ca-certificates - curl gnupg-agent software-properties-common -y - - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - - - add-apt-repository \"deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\" - - curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - - - echo 'deb https://apt.kubernetes.io/ kubernetes-xenial main' > /etc/apt/sources.list.d/kubernetes.list - - apt update -y - - apt install docker-ce docker-ce-cli containerd.io kubelet kubeadm kubectl -y - - systemctl enable --now docker kubelet - - usermod -aG docker ubuntu - files: - - path: /etc/netplan/50-cloud-init.yaml - owner: root:root - permissions: '0644' - content: | - network: - ethernets: - enp1s0: - dhcp4: true - enp2s0: - dhcp4: true - version: 2 -" -``` - -## Pivoting Ironic - -Before running the move command of Clusterctl, elements such as Baremetal -Operator, Ironic if applicable, and the BareMetalHost CRs need to be moved to -the target cluster. During the move, the BareMetalHost object must be annotated -with `baremetalhost.metal3.io/paused` key. The value does not matter. The -presence of this annotation will stop the reconciliation loop for that object. - -In addition, it is critical that the move of the BMHs does not lead to a lost -status for those objects. If the status is lost, BMO will register the nodes -as available, and introspect them again. - -More information TBA on move commands. diff --git a/docs/images/components.png b/docs/images/components.png deleted file mode 100644 index 592e10b7264a83c63e090c186668e9eaa34101e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90895 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 z 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 diff --git a/docs/images/controllerssequencediagram.png b/docs/images/controllerssequencediagram.png deleted file mode 100644 index 9423577eb34895e4ea40317e7e33a6115bedac98..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55662 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 Jb{_ioe*t2{t=Rwo diff --git a/docs/images/fields_mapping.png b/docs/images/fields_mapping.png deleted file mode 100644 index fdb2a12651a1d2b1a9e4f53bda76ad8f05798082..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48250 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 aJ^u8=U2&yBZr0U;k2e;-6;u_V`tTo>#Y=bq diff --git a/docs/releasing.md b/docs/releasing.md index 1aad7d13..48f7cd91 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -25,18 +25,14 @@ You must have docker installed. 1. A container image of the shared cluster-api-provider-metal3 manager 1. A git tag -1. A release on Github containing: - - A manifest file - `infrastructure-components.yaml` - - A metadata file - `metadata.yaml` - - A cluster template - `cluster-template.yaml` ### Artifact locations 1. The container image is found in the registry `quay.io/metal3-io` with an image - name of `cluster-api-provider-metal3` and a tag that matches the release + name of `ipam` and a tag that matches the release version. The image is automatically built once the release has been created. -## Creating a release for CAPM3 +## Creating a release for IPAM ### Process @@ -66,22 +62,19 @@ Releasing requires a particular set of permissions. Multiple additional actions are required in the Metal3 project -### Update the Jenkins jobs +### Update CAPM3 -For each minor or major release, two jobs need to be created : - -- a master job that runs on a regular basis -- a PR verification job that is triggered by a keyword on a PR targeted for that - release branch. +CAPM3 should use the latest version. Changes are required there to pull the +latest version. ### Update Metal3-dev-env Metal3-dev-env variables need to be modified. After a major or minor release, -the new minor version (that follows CAPI versioning) should point to master for -CAPM3 and the released version should point to the release branch. +the new minor version should point to master for +IPAM and the released version should point to the release branch. -### Update the image of CAPM3 in the release branch +### Update the image of IPAM in the release branch If you just created a release branch (i.e. minor version release), you should -modify the image for CAPM3 deployment in this branch to be tagged with the +modify the image for IPAM deployment in this branch to be tagged with the branch name. The image will then follow the branch. diff --git a/examples/addons.yaml b/examples/addons.yaml deleted file mode 100644 index 4490c96a..00000000 --- a/examples/addons.yaml +++ /dev/null @@ -1,783 +0,0 @@ ---- -# Source: calico/templates/calico-config.yaml -# This ConfigMap is used to configure a self-hosted Calico installation. -kind: ConfigMap -apiVersion: v1 -metadata: - name: calico-config - namespace: kube-system -data: - # Typha is disabled. - typha_service_name: "none" - # Configure the backend to use. - calico_backend: "bird" - - # Configure the MTU to use - veth_mtu: "1440" - - # The CNI network configuration to install on each node. The special - # values in this config will be automatically populated. - cni_network_config: |- - { - "name": "k8s-pod-network", - "cniVersion": "0.3.1", - "plugins": [ - { - "type": "calico", - "log_level": "info", - "datastore_type": "kubernetes", - "nodename": "__KUBERNETES_NODE_NAME__", - "mtu": __CNI_MTU__, - "ipam": { - "type": "calico-ipam" - }, - "policy": { - "type": "k8s" - }, - "kubernetes": { - "kubeconfig": "__KUBECONFIG_FILEPATH__" - } - }, - { - "type": "portmap", - "snat": true, - "capabilities": {"portMappings": true} - } - ] - } - ---- -# Source: calico/templates/kdd-crds.yaml -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: felixconfigurations.crd.projectcalico.org -spec: - scope: Cluster - group: crd.projectcalico.org - version: v1 - names: - kind: FelixConfiguration - plural: felixconfigurations - singular: felixconfiguration ---- - -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: ipamblocks.crd.projectcalico.org -spec: - scope: Cluster - group: crd.projectcalico.org - version: v1 - names: - kind: IPAMBlock - plural: ipamblocks - singular: ipamblock - ---- - -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: blockaffinities.crd.projectcalico.org -spec: - scope: Cluster - group: crd.projectcalico.org - version: v1 - names: - kind: BlockAffinity - plural: blockaffinities - singular: blockaffinity - ---- - -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: ipamhandles.crd.projectcalico.org -spec: - scope: Cluster - group: crd.projectcalico.org - version: v1 - names: - kind: IPAMHandle - plural: ipamhandles - singular: ipamhandle - ---- - -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: ipamconfigs.crd.projectcalico.org -spec: - scope: Cluster - group: crd.projectcalico.org - version: v1 - names: - kind: IPAMConfig - plural: ipamconfigs - singular: ipamconfig - ---- - -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: bgppeers.crd.projectcalico.org -spec: - scope: Cluster - group: crd.projectcalico.org - version: v1 - names: - kind: BGPPeer - plural: bgppeers - singular: bgppeer - ---- - -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: bgpconfigurations.crd.projectcalico.org -spec: - scope: Cluster - group: crd.projectcalico.org - version: v1 - names: - kind: BGPConfiguration - plural: bgpconfigurations - singular: bgpconfiguration - ---- - -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: ippools.crd.projectcalico.org -spec: - scope: Cluster - group: crd.projectcalico.org - version: v1 - names: - kind: IPPool - plural: ippools - singular: ippool - ---- - -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: hostendpoints.crd.projectcalico.org -spec: - scope: Cluster - group: crd.projectcalico.org - version: v1 - names: - kind: HostEndpoint - plural: hostendpoints - singular: hostendpoint - ---- - -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: clusterinformations.crd.projectcalico.org -spec: - scope: Cluster - group: crd.projectcalico.org - version: v1 - names: - kind: ClusterInformation - plural: clusterinformations - singular: clusterinformation - ---- - -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: globalnetworkpolicies.crd.projectcalico.org -spec: - scope: Cluster - group: crd.projectcalico.org - version: v1 - names: - kind: GlobalNetworkPolicy - plural: globalnetworkpolicies - singular: globalnetworkpolicy - ---- - -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: globalnetworksets.crd.projectcalico.org -spec: - scope: Cluster - group: crd.projectcalico.org - version: v1 - names: - kind: GlobalNetworkSet - plural: globalnetworksets - singular: globalnetworkset - ---- - -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: networkpolicies.crd.projectcalico.org -spec: - scope: Namespaced - group: crd.projectcalico.org - version: v1 - names: - kind: NetworkPolicy - plural: networkpolicies - singular: networkpolicy - ---- - -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: networksets.crd.projectcalico.org -spec: - scope: Namespaced - group: crd.projectcalico.org - version: v1 - names: - kind: NetworkSet - plural: networksets - singular: networkset ---- -# Source: calico/templates/rbac.yaml - -# Include a clusterrole for the kube-controllers component, -# and bind it to the calico-kube-controllers serviceaccount. -kind: ClusterRole -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: calico-kube-controllers -rules: - # Nodes are watched to monitor for deletions. - - apiGroups: [""] - resources: - - nodes - verbs: - - watch - - list - - get - # Pods are queried to check for existence. - - apiGroups: [""] - resources: - - pods - verbs: - - get - # IPAM resources are manipulated when nodes are deleted. - - apiGroups: ["crd.projectcalico.org"] - resources: - - ippools - verbs: - - list - - apiGroups: ["crd.projectcalico.org"] - resources: - - blockaffinities - - ipamblocks - - ipamhandles - verbs: - - get - - list - - create - - update - - delete - # Needs access to update clusterinformations. - - apiGroups: ["crd.projectcalico.org"] - resources: - - clusterinformations - verbs: - - get - - create - - update ---- -kind: ClusterRoleBinding -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: calico-kube-controllers -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: calico-kube-controllers -subjects: -- kind: ServiceAccount - name: calico-kube-controllers - namespace: kube-system ---- -# Include a clusterrole for the calico-node DaemonSet, -# and bind it to the calico-node serviceaccount. -kind: ClusterRole -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: calico-node -rules: - # The CNI plugin needs to get pods, nodes, and namespaces. - - apiGroups: [""] - resources: - - pods - - nodes - - namespaces - verbs: - - get - - apiGroups: [""] - resources: - - endpoints - - services - verbs: - # Used to discover service IPs for advertisement. - - watch - - list - # Used to discover Typhas. - - get - - apiGroups: [""] - resources: - - nodes/status - verbs: - # Needed for clearing NodeNetworkUnavailable flag. - - patch - # Calico stores some configuration information in node annotations. - - update - # Watch for changes to Kubernetes NetworkPolicies. - - apiGroups: ["networking.k8s.io"] - resources: - - networkpolicies - verbs: - - watch - - list - # Used by Calico for policy information. - - apiGroups: [""] - resources: - - pods - - namespaces - - serviceaccounts - verbs: - - list - - watch - # The CNI plugin patches pods/status. - - apiGroups: [""] - resources: - - pods/status - verbs: - - patch - # Calico monitors various CRDs for config. - - apiGroups: ["crd.projectcalico.org"] - resources: - - globalfelixconfigs - - felixconfigurations - - bgppeers - - globalbgpconfigs - - bgpconfigurations - - ippools - - ipamblocks - - globalnetworkpolicies - - globalnetworksets - - networkpolicies - - networksets - - clusterinformations - - hostendpoints - verbs: - - get - - list - - watch - # Calico must create and update some CRDs on startup. - - apiGroups: ["crd.projectcalico.org"] - resources: - - ippools - - felixconfigurations - - clusterinformations - verbs: - - create - - update - # Calico stores some configuration information on the node. - - apiGroups: [""] - resources: - - nodes - verbs: - - get - - list - - watch - # These permissions are only requried for upgrade from v2.6, and can - # be removed after upgrade or on fresh installations. - - apiGroups: ["crd.projectcalico.org"] - resources: - - bgpconfigurations - - bgppeers - verbs: - - create - - update - # These permissions are required for Calico CNI to perform IPAM allocations. - - apiGroups: ["crd.projectcalico.org"] - resources: - - blockaffinities - - ipamblocks - - ipamhandles - verbs: - - get - - list - - create - - update - - delete - - apiGroups: ["crd.projectcalico.org"] - resources: - - ipamconfigs - verbs: - - get - # Block affinities must also be watchable by confd for route aggregation. - - apiGroups: ["crd.projectcalico.org"] - resources: - - blockaffinities - verbs: - - watch - # The Calico IPAM migration needs to get daemonsets. These permissions can be - # removed if not upgrading from an installation using host-local IPAM. - - apiGroups: ["apps"] - resources: - - daemonsets - verbs: - - get ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: calico-node -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: calico-node -subjects: -- kind: ServiceAccount - name: calico-node - namespace: kube-system - ---- -# Source: calico/templates/calico-node.yaml -# This manifest installs the calico-node container, as well -# as the CNI plugins and network config on -# each master and worker node in a Kubernetes cluster. -kind: DaemonSet -apiVersion: apps/v1 -metadata: - name: calico-node - namespace: kube-system - labels: - k8s-app: calico-node -spec: - selector: - matchLabels: - k8s-app: calico-node - updateStrategy: - type: RollingUpdate - rollingUpdate: - maxUnavailable: 1 - template: - metadata: - labels: - k8s-app: calico-node - annotations: - # This, along with the CriticalAddonsOnly toleration below, - # marks the pod as a critical add-on, ensuring it gets - # priority scheduling and that its resources are reserved - # if it ever gets evicted. - scheduler.alpha.kubernetes.io/critical-pod: '' - spec: - nodeSelector: - beta.kubernetes.io/os: linux - hostNetwork: true - tolerations: - # Make sure calico-node gets scheduled on all nodes. - - effect: NoSchedule - operator: Exists - # Mark the pod as a critical add-on for rescheduling. - - key: CriticalAddonsOnly - operator: Exists - - effect: NoExecute - operator: Exists - serviceAccountName: calico-node - # Minimize downtime during a rolling upgrade or deletion; tell Kubernetes to do a "force - # deletion": https://kubernetes.io/docs/concepts/workloads/pods/pod/#termination-of-pods. - terminationGracePeriodSeconds: 0 - priorityClassName: system-node-critical - initContainers: - # This container performs upgrade from host-local IPAM to calico-ipam. - # It can be deleted if this is a fresh installation, or if you have already - # upgraded to use calico-ipam. - - name: upgrade-ipam - image: calico/cni:v3.8.2 - command: ["/opt/cni/bin/calico-ipam", "-upgrade"] - env: - - name: KUBERNETES_NODE_NAME - valueFrom: - fieldRef: - fieldPath: spec.nodeName - - name: CALICO_NETWORKING_BACKEND - valueFrom: - configMapKeyRef: - name: calico-config - key: calico_backend - volumeMounts: - - mountPath: /var/lib/cni/networks - name: host-local-net-dir - - mountPath: /host/opt/cni/bin - name: cni-bin-dir - # This container installs the CNI binaries - # and CNI network config file on each node. - - name: install-cni - image: calico/cni:v3.8.2 - command: ["/install-cni.sh"] - env: - # Name of the CNI config file to create. - - name: CNI_CONF_NAME - value: "10-calico.conflist" - # The CNI network config to install on each node. - - name: CNI_NETWORK_CONFIG - valueFrom: - configMapKeyRef: - name: calico-config - key: cni_network_config - # Set the hostname based on the k8s node name. - - name: KUBERNETES_NODE_NAME - valueFrom: - fieldRef: - fieldPath: spec.nodeName - # CNI MTU Config variable - - name: CNI_MTU - valueFrom: - configMapKeyRef: - name: calico-config - key: veth_mtu - # Prevents the container from sleeping forever. - - name: SLEEP - value: "false" - volumeMounts: - - mountPath: /host/opt/cni/bin - name: cni-bin-dir - - mountPath: /host/etc/cni/net.d - name: cni-net-dir - # Adds a Flex Volume Driver that creates a per-pod Unix Domain Socket to allow Dikastes - # to communicate with Felix over the Policy Sync API. - - name: flexvol-driver - image: calico/pod2daemon-flexvol:v3.8.2 - volumeMounts: - - name: flexvol-driver-host - mountPath: /host/driver - containers: - # Runs calico-node container on each Kubernetes node. This - # container programs network policy and routes on each - # host. - - name: calico-node - image: calico/node:v3.8.2 - env: - # Use Kubernetes API as the backing datastore. - - name: DATASTORE_TYPE - value: "kubernetes" - # Wait for the datastore. - - name: WAIT_FOR_DATASTORE - value: "true" - # Set based on the k8s node name. - - name: NODENAME - valueFrom: - fieldRef: - fieldPath: spec.nodeName - # Choose the backend to use. - - name: CALICO_NETWORKING_BACKEND - valueFrom: - configMapKeyRef: - name: calico-config - key: calico_backend - # Cluster type to identify the deployment type - - name: CLUSTER_TYPE - value: "k8s,bgp" - # Auto-detect the BGP IP address. - - name: IP - value: "autodetect" - # Enable IPIP - - name: CALICO_IPV4POOL_IPIP - value: "Always" - # Set MTU for tunnel device used if ipip is enabled - - name: FELIX_IPINIPMTU - valueFrom: - configMapKeyRef: - name: calico-config - key: veth_mtu - # The default IPv4 pool to create on startup if none exists. Pod IPs will be - # chosen from this range. Changing this value after installation will have - # no effect. This should fall within `--cluster-cidr`. - - name: CALICO_IPV4POOL_CIDR - value: "192.168.0.0/16" - # Disable file logging so `kubectl logs` works. - - name: CALICO_DISABLE_FILE_LOGGING - value: "true" - # Set Felix endpoint to host default action to ACCEPT. - - name: FELIX_DEFAULTENDPOINTTOHOSTACTION - value: "ACCEPT" - # Disable IPv6 on Kubernetes. - - name: FELIX_IPV6SUPPORT - value: "false" - # Set Felix logging to "info" - - name: FELIX_LOGSEVERITYSCREEN - value: "info" - - name: FELIX_HEALTHENABLED - value: "true" - securityContext: - privileged: true - resources: - requests: - cpu: 250m - livenessProbe: - httpGet: - path: /liveness - port: 9099 - host: localhost - periodSeconds: 10 - initialDelaySeconds: 10 - failureThreshold: 6 - readinessProbe: - exec: - command: - - /bin/calico-node - - -bird-ready - - -felix-ready - periodSeconds: 10 - volumeMounts: - - mountPath: /lib/modules - name: lib-modules - readOnly: true - - mountPath: /run/xtables.lock - name: xtables-lock - readOnly: false - - mountPath: /var/run/calico - name: var-run-calico - readOnly: false - - mountPath: /var/lib/calico - name: var-lib-calico - readOnly: false - - name: policysync - mountPath: /var/run/nodeagent - volumes: - # Used by calico-node. - - name: lib-modules - hostPath: - path: /lib/modules - - name: var-run-calico - hostPath: - path: /var/run/calico - - name: var-lib-calico - hostPath: - path: /var/lib/calico - - name: xtables-lock - hostPath: - path: /run/xtables.lock - type: FileOrCreate - # Used to install CNI. - - name: cni-bin-dir - hostPath: - path: /opt/cni/bin - - name: cni-net-dir - hostPath: - path: /etc/cni/net.d - # Mount in the directory for host-local IPAM allocations. This is - # used when upgrading from host-local to calico-ipam, and can be removed - # if not using the upgrade-ipam init container. - - name: host-local-net-dir - hostPath: - path: /var/lib/cni/networks - # Used to create per-pod Unix Domain Sockets - - name: policysync - hostPath: - type: DirectoryOrCreate - path: /var/run/nodeagent - # Used to install Flex Volume Driver - - name: flexvol-driver-host - hostPath: - type: DirectoryOrCreate - path: /usr/libexec/kubernetes/kubelet-plugins/volume/exec/nodeagent~uds ---- - -apiVersion: v1 -kind: ServiceAccount -metadata: - name: calico-node - namespace: kube-system - ---- -# Source: calico/templates/calico-kube-controllers.yaml - -# See https://github.com/projectcalico/kube-controllers -apiVersion: apps/v1 -kind: Deployment -metadata: - name: calico-kube-controllers - namespace: kube-system - labels: - k8s-app: calico-kube-controllers -spec: - # The controllers can only have a single active instance. - replicas: 1 - selector: - matchLabels: - k8s-app: calico-kube-controllers - strategy: - type: Recreate - template: - metadata: - name: calico-kube-controllers - namespace: kube-system - labels: - k8s-app: calico-kube-controllers - annotations: - scheduler.alpha.kubernetes.io/critical-pod: '' - spec: - nodeSelector: - beta.kubernetes.io/os: linux - tolerations: - # Mark the pod as a critical add-on for rescheduling. - - key: CriticalAddonsOnly - operator: Exists - - key: node-role.kubernetes.io/master - effect: NoSchedule - serviceAccountName: calico-kube-controllers - priorityClassName: system-cluster-critical - containers: - - name: calico-kube-controllers - image: calico/kube-controllers:v3.8.2 - env: - # Choose which controllers to run. - - name: ENABLED_CONTROLLERS - value: node - - name: DATASTORE_TYPE - value: kubernetes - readinessProbe: - exec: - command: - - /usr/bin/check-status - - -r - ---- - -apiVersion: v1 -kind: ServiceAccount -metadata: - name: calico-kube-controllers - namespace: kube-system ---- -# Source: calico/templates/calico-etcd-secrets.yaml - ---- -# Source: calico/templates/calico-typha.yaml - ---- -# Source: calico/templates/configure-canal.yaml - diff --git a/examples/cluster/cluster.yaml b/examples/cluster/cluster.yaml deleted file mode 100644 index ac66f9d1..00000000 --- a/examples/cluster/cluster.yaml +++ /dev/null @@ -1,29 +0,0 @@ ---- -apiVersion: cluster.x-k8s.io/v1alpha3 -kind: Cluster -metadata: - name: ${CLUSTER_NAME} -spec: - clusterNetwork: - services: - cidrBlocks: ["10.96.0.0/12"] - pods: - cidrBlocks: ["192.168.0.0/16"] - serviceDomain: "cluster.local" - infrastructureRef: - apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 - kind: Metal3Cluster - name: ${CLUSTER_NAME} - controlPlaneRef: - kind: KubeadmControlPlane - apiVersion: controlplane.cluster.x-k8s.io/v1alpha3 - name: ${CLUSTER_NAME}-controlplane ---- -apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 -kind: Metal3Cluster -metadata: - name: ${CLUSTER_NAME} -spec: - controlPlaneEndpoint: - host: 192.168.111.249 - port: 6443 diff --git a/examples/cluster/kustomizeconfig.yaml b/examples/cluster/kustomizeconfig.yaml deleted file mode 100644 index 008398b4..00000000 --- a/examples/cluster/kustomizeconfig.yaml +++ /dev/null @@ -1,6 +0,0 @@ -namespace: -- kind: Cluster - group: cluster.x-k8s.io - version: v1alpha3 - path: spec/infrastructureRef/namespace - create: true diff --git a/examples/clusterctl-templates/clusterctl-cluster.yaml b/examples/clusterctl-templates/clusterctl-cluster.yaml deleted file mode 100644 index 809d7345..00000000 --- a/examples/clusterctl-templates/clusterctl-cluster.yaml +++ /dev/null @@ -1,128 +0,0 @@ -apiVersion: cluster.x-k8s.io/v1alpha3 -kind: Cluster -metadata: - name: ${ CLUSTER_NAME } - namespace: ${ NAMESPACE } -spec: - clusterNetwork: - services: - cidrBlocks: [${ SERVICE_CIDR }] - pods: - cidrBlocks: [${ POD_CIDR }] - infrastructureRef: - apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 - kind: Metal3Cluster - name: ${ CLUSTER_NAME } - controlPlaneRef: - kind: KubeadmControlPlane - apiVersion: controlplane.cluster.x-k8s.io/v1alpha3 - name: ${ CLUSTER_NAME } ---- -apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 -kind: Metal3Cluster -metadata: - name: ${ CLUSTER_NAME } - namespace: ${ NAMESPACE } -spec: - controlPlaneEndpoint: - host: ${ API_ENDPOINT_HOST } - port: ${ API_ENDPOINT_PORT } - noCloudProvider: true ---- -kind: KubeadmControlPlane -apiVersion: controlplane.cluster.x-k8s.io/v1alpha3 -metadata: - name: ${ CLUSTER_NAME } - namespace: ${ NAMESPACE } -spec: - replicas: ${ CONTROL_PLANE_MACHINE_COUNT } - version: ${ KUBERNETES_VERSION } - infrastructureTemplate: - kind: Metal3MachineTemplate - apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 - name: ${ CLUSTER_NAME }-controlplane - kubeadmConfigSpec: - joinConfiguration: - controlPlane: {} - nodeRegistration: - name: '{{ ds.meta_data.name }}' - kubeletExtraArgs: - node-labels: 'metal3.io/uuid={{ ds.meta_data.uuid }}' - initConfiguration: - nodeRegistration: - name: '{{ ds.meta_data.name }}' - kubeletExtraArgs: - node-labels: 'metal3.io/uuid={{ ds.meta_data.uuid }}' -${ CTLPLANE_KUBEADM_EXTRA_CONFIG } ---- -apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 -kind: Metal3MachineTemplate -metadata: - name: ${ CLUSTER_NAME }-controlplane - namespace: ${ NAMESPACE } -spec: - template: - spec: - image: - url: ${ IMAGE_URL } - checksum: ${ IMAGE_CHECKSUM } ---- -apiVersion: cluster.x-k8s.io/v1alpha3 -kind: MachineDeployment -metadata: - name: ${ CLUSTER_NAME } - namespace: ${ NAMESPACE } - labels: - cluster.x-k8s.io/cluster-name: ${ CLUSTER_NAME } - nodepool: nodepool-0 -spec: - clusterName: ${ CLUSTER_NAME } - replicas: ${ WORKER_MACHINE_COUNT } - selector: - matchLabels: - cluster.x-k8s.io/cluster-name: ${ CLUSTER_NAME } - nodepool: nodepool-0 - template: - metadata: - labels: - cluster.x-k8s.io/cluster-name: ${ CLUSTER_NAME } - nodepool: nodepool-0 - spec: - clusterName: ${ CLUSTER_NAME } - version: ${ KUBERNETES_VERSION } - bootstrap: - configRef: - name: ${ CLUSTER_NAME }-workers - apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3 - kind: KubeadmConfigTemplate - infrastructureRef: - name: ${ CLUSTER_NAME }-workers - apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 - kind: Metal3MachineTemplate ---- -apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 -kind: Metal3MachineTemplate -metadata: - name: ${ CLUSTER_NAME }-workers - namespace: ${ NAMESPACE } -spec: - template: - spec: - image: - url: ${ IMAGE_URL } - checksum: ${ IMAGE_CHECKSUM } ---- -apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3 -kind: KubeadmConfigTemplate -metadata: - name: ${ CLUSTER_NAME }-workers - namespace: ${ NAMESPACE } -spec: - template: - spec: - joinConfiguration: - nodeRegistration: - name: '{{ ds.meta_data.name }}' - kubeletExtraArgs: - node-labels: 'metal3.io/uuid={{ ds.meta_data.uuid }}' -${ WORKERS_KUBEADM_EXTRA_CONFIG } diff --git a/examples/clusterctl-templates/example_variables.rc b/examples/clusterctl-templates/example_variables.rc deleted file mode 100644 index 19a45fb2..00000000 --- a/examples/clusterctl-templates/example_variables.rc +++ /dev/null @@ -1,105 +0,0 @@ -export POD_CIDR='"192.168.0.0/24"' -export SERVICE_CIDR='"10.96.0.0/12"' -export API_ENDPOINT_HOST="192.168.111.249" -export API_ENDPOINT_PORT="6443" -export IMAGE_URL="http://192.168.0.1/ubuntu.qcow2" -export IMAGE_CHECKSUM="http://192.168.0.1/ubuntu.qcow2.md5sum" -export CTLPLANE_KUBEADM_EXTRA_CONFIG=" - preKubeadmCommands: - - ip link set dev enp2s0 up - - dhclient enp2s0 - - apt update -y - - netplan apply - - >- - apt install net-tools gcc linux-headers-$(uname -r) bridge-utils - apt-transport-https ca-certificates curl gnupg-agent - software-properties-common -y - - apt install -y keepalived && systemctl stop keepalived - - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - - - add-apt-repository \"deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\" - - curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - - - echo 'deb https://apt.kubernetes.io/ kubernetes-xenial main' > /etc/apt/sources.list.d/kubernetes.list - - apt update -y - - apt install docker-ce docker-ce-cli containerd.io kubelet kubeadm kubectl -y - - systemctl enable --now docker kubelet - - if (curl -sk --max-time 10 https://{{ CLUSTER_APIENDPOINT_HOST }}:6443/healthz); then echo \"keepalived already running\";else systemctl start keepalived; fi - - usermod -aG docker ubuntu - postKubeadmCommands: - - mkdir -p /home/ubuntu/.kube - - cp /etc/kubernetes/admin.conf /home/ubuntu/.kube/config - - systemctl enable --now keepalived - - chown ubuntu:ubuntu /home/ubuntu/.kube/config - files: - - path: /etc/keepalived/keepalived.conf - content: | - ! Configuration File for keepalived - global_defs { - notification_email { - sysadmin@example.com - support@example.com - } - notification_email_from lb@example.com - smtp_server localhost - smtp_connect_timeout 30 - } - vrrp_instance VI_2 { - state MASTER - interface enp2s0 - virtual_router_id 2 - priority 101 - advert_int 1 - virtual_ipaddress { - {{ CLUSTER_APIENDPOINT_HOST }} - } - } - - path: /etc/netplan/50-cloud-init.yaml - owner: root:root - permissions: '0644' - content: | - network: - ethernets: - enp2s0: - dhcp4: true - version: 2 - - path : /etc/netplan/60-ironicendpoint.yaml - owner: root:root - permissions: '0644' - content: | - network: - version: 2 - renderer: networkd - bridges: - ironicendpoint: - interfaces: [enp1s0] - dhcp4: yes -" -export WORKERS_KUBEADM_EXTRA_CONFIG=" - preKubeadmCommands: - - ip link set dev enp2s0 up - - dhclient enp2s0 - - apt update -y - - netplan apply - - >- - apt install apt-transport-https ca-certificates - curl gnupg-agent software-properties-common -y - - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - - - add-apt-repository \"deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\" - - curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - - - echo 'deb https://apt.kubernetes.io/ kubernetes-xenial main' > /etc/apt/sources.list.d/kubernetes.list - - apt update -y - - apt install docker-ce docker-ce-cli containerd.io kubelet kubeadm kubectl -y - - systemctl enable --now docker kubelet - - usermod -aG docker ubuntu - files: - - path: /etc/netplan/50-cloud-init.yaml - owner: root:root - permissions: '0644' - content: | - network: - ethernets: - enp1s0: - dhcp4: true - enp2s0: - dhcp4: true - version: 2 -" diff --git a/examples/controlplane/controlplane.yaml b/examples/controlplane/controlplane.yaml deleted file mode 100644 index 2e3d6d24..00000000 --- a/examples/controlplane/controlplane.yaml +++ /dev/null @@ -1,180 +0,0 @@ -kind: KubeadmControlPlane -apiVersion: controlplane.cluster.x-k8s.io/v1alpha3 -metadata: - name: ${CLUSTER_NAME}-controlplane -spec: - replicas: 3 - version: v1.17.0 - infrastructureTemplate: - kind: Metal3MachineTemplate - apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 - name: ${CLUSTER_NAME}-controlplane - kubeadmConfigSpec: - initConfiguration: - nodeRegistration: - name: '{{ ds.meta_data.local_hostname }}' - kubeletExtraArgs: - cloud-provider: baremetal - clusterConfiguration: - apiServer: - extraArgs: - cloud-provider: baremetal - controllerManager: - extraArgs: - cloud-provider: baremetal - joinConfiguration: - controlPlane: {} - nodeRegistration: - name: '{{ ds.meta_data.local_hostname }}' - kubeletExtraArgs: - cloud-provider: baremetal ---- -apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 -kind: Metal3MachineTemplate -metadata: - name: ${CLUSTER_NAME}-controlplane -spec: - template: - spec: - image: - url: "http://172.22.0.1/images/rhcos-ootpa-latest.qcow2" - checksum: "97830b21ed272a3d854615beb54cf004" - dataTemplate: - name: ${CLUSTER_NAME}-cp-metadata ---- -apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 -kind: Metal3DataTemplate -metadata: - name: ${CLUSTER_NAME}-cp-metadata -spec: - clusterName: ${CLUSTER_NAME} - metaData: - strings: - - key: abc - value: def - indexes: - - key: index_0_1 - offset: 0 - step: 1 - - key: index_3 - offset: 3 - - key: index_5_2 - offset: 5 - step: 2 - objectNames: - - key: machine_name - object: machine - - key: metal3machine_name - object: metal3machine - - key: bmh_name - object: baremetalhost - ipAddressesFromPool: - - key: ip_1 - name: pool1 - - key: ip_2 - name: pool2 - - key: ip6_1 - name: pool6-1 - - key: ip6_2 - name: pool6-2 - prefixesFromPool: - - key: prefix_1 - name: pool1 - - key: prefix_2 - name: pool2 - - key: prefix6_1 - name: pool6-1 - - key: prefix6_2 - name: pool6-2 - gatewaysFromPool: - - key: gateway_1 - name: pool1 - - key: gateway_2 - name: pool2 - - key: gateway6_1 - name: pool6-1 - - key: gateway6_2 - name: pool6-2 - fromHostInterfaces: - - key: mac - interface: eth0 - networkData: - links: - ethernets: - - type: phy - id: enp1s0 - macAddress: - fromHostInterface: eth0 - - type: phy - id: enp2s0 - macAddress: - fromHostInterface: eth1 - networks: - ipv4DHCP: - - id: provisioning - link: enp1s0 - ipv4: - - id: baremetal - link: enp2s0 - ipAddressFromIPPool: pool1 - routes: - - network: 0.0.0.0 - prefix: 0 - gateway: - fromIPPool: pool1 - services: - dns: - - 8.8.4.4 - services: - dns: - - 8.8.8.8 ---- -apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 -kind: Metal3IPPool -metadata: - name: pool1 -spec: - clusterName: ${CLUSTER_NAME} - pools: - - start: 192.168.0.10 - end: 192.168.0.250 - prefix: 24 - gateway: 192.168.0.1 - namePrefix: pool1 ---- -apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 -kind: Metal3IPPool -metadata: - name: pool2 -spec: - clusterName: ${CLUSTER_NAME} - pools: - - subnet: 192.168.1.0/24 - prefix: 25 - gateway: 192.168.1.1 - namePrefix: pool2 ---- -apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 -kind: Metal3IPPool -metadata: - name: pool6-1 -spec: - clusterName: ${CLUSTER_NAME} - pools: - - start: 2001::10 - end: 2001::ff00 - prefix: 96 - gateway: 2001::1 - namePrefix: pool6-1 ---- -apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 -kind: Metal3IPPool -metadata: - name: pool6-2 -spec: - clusterName: ${CLUSTER_NAME} - pools: - - subnet: 2001:ABC::0/96 - prefix: 96 - gateway: 2001:ABC::1 - namePrefix: pool6-2 diff --git a/examples/controlplane/kustomization.yaml b/examples/controlplane/kustomization.yaml deleted file mode 100644 index 6a0c202d..00000000 --- a/examples/controlplane/kustomization.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -namespace: default -resources: -- controlplane.yaml -configurations: -- kustomizeconfig.yaml diff --git a/examples/controlplane/kustomizeconfig.yaml b/examples/controlplane/kustomizeconfig.yaml deleted file mode 100644 index 7e2a7baf..00000000 --- a/examples/controlplane/kustomizeconfig.yaml +++ /dev/null @@ -1,15 +0,0 @@ -namespace: -- kind: Machine - group: cluster.x-k8s.io - version: v1alpha3 - path: spec/infrastructureRef/namespace - create: true -- kind: Machine - group: cluster.x-k8s.io - version: v1alpha3 - path: spec/bootstrap/configRef/namespace - create: true - -commonLabels: -- path: metadata/labels - create: true diff --git a/examples/generate.sh b/examples/generate.sh index 7ff8a064..573796f5 100755 --- a/examples/generate.sh +++ b/examples/generate.sh @@ -26,26 +26,14 @@ command -v "${ENVSUBST}" >/dev/null 2>&1 || echo -v "Cannot find ${ENVSUBST} in # Cluster. export CLUSTER_NAME="${CLUSTER_NAME:-test1}" -export KUBERNETES_VERSION="${KUBERNETES_VERSION:-v1.16.0}" -# Machine settings. -export CONTROL_PLANE_MACHINE_TYPE="${CONTROL_PLANE_MACHINE_TYPE:-t2.medium}" -export NODE_MACHINE_TYPE="${CONTROL_PLANE_MACHINE_TYPE:-t2.medium}" -export SSH_KEY_NAME="${SSH_KEY_NAME:-default}" # Outputs. COMPONENTS_CERT_MANAGER_GENERATED_FILE=${OUTPUT_DIR}/cert-manager.yaml -COMPONENTS_CLUSTER_API_GENERATED_FILE=${SOURCE_DIR}/provider-components/core-components.yaml -COMPONENTS_KUBEADM_GENERATED_FILE=${SOURCE_DIR}/provider-components/bootstrap-components.yaml -COMPONENTS_CTRLPLANE_GENERATED_FILE=${SOURCE_DIR}/provider-components/ctlplane-components.yaml COMPONENTS_METAL3_GENERATED_FILE=${SOURCE_DIR}/provider-components/infrastructure-components.yaml PROVIDER_COMPONENTS_GENERATED_FILE=${OUTPUT_DIR}/provider-components.yaml -CLUSTER_GENERATED_FILE=${OUTPUT_DIR}/cluster.yaml -CONTROLPLANE_GENERATED_FILE=${OUTPUT_DIR}/controlplane.yaml -MACHINEDEPLOYMENT_GENERATED_FILE=${OUTPUT_DIR}/machinedeployment.yaml -METAL3PLANE_GENERATED_FILE=${OUTPUT_DIR}/metal3plane.yaml -METAL3CRDS_GENERATED_FILE=${OUTPUT_DIR}/metal3crds.yaml +IPPOOL_GENERATED_FILE=${OUTPUT_DIR}/ippool.yaml # Overwrite flag. OVERWRITE=0 @@ -85,40 +73,12 @@ fi mkdir -p "${OUTPUT_DIR}" # Generate cluster resources. -kustomize build "${SOURCE_DIR}/cluster" | envsubst > "${CLUSTER_GENERATED_FILE}" -echo "Generated ${CLUSTER_GENERATED_FILE}" - -# Generate controlplane resources. -kustomize build "${SOURCE_DIR}/controlplane" | envsubst > "${CONTROLPLANE_GENERATED_FILE}" -echo "Generated ${CONTROLPLANE_GENERATED_FILE}" - -# Generate metal3crds resources. -kustomize build "${SOURCE_DIR}/metal3crds" | envsubst > "${METAL3CRDS_GENERATED_FILE}" -echo "Generated ${METAL3CRDS_GENERATED_FILE}" - -# Generate metal3plane resources. -kustomize build "${SOURCE_DIR}/metal3plane" | envsubst > "${METAL3PLANE_GENERATED_FILE}" -echo "Generated ${METAL3PLANE_GENERATED_FILE}" - -# Generate machinedeployment resources. -kustomize build "${SOURCE_DIR}/machinedeployment" | envsubst >> "${MACHINEDEPLOYMENT_GENERATED_FILE}" -echo "Generated ${MACHINEDEPLOYMENT_GENERATED_FILE}" +kustomize build "${SOURCE_DIR}/ippool" | envsubst > "${IPPOOL_GENERATED_FILE}" +echo "Generated ${IPPOOL_GENERATED_FILE}" # Get Cert-manager provider components file curl -L -o "${COMPONENTS_CERT_MANAGER_GENERATED_FILE}" https://github.com/jetstack/cert-manager/releases/download/v0.13.0/cert-manager.yaml -# Generate Cluster API provider components file. -kustomize build "github.com/kubernetes-sigs/cluster-api/config/?ref=master" > "${COMPONENTS_CLUSTER_API_GENERATED_FILE}" -echo "Generated ${COMPONENTS_CLUSTER_API_GENERATED_FILE}" - -# Generate Kubeadm Bootstrap Provider components file. -kustomize build "github.com/kubernetes-sigs/cluster-api/bootstrap/kubeadm/config/?ref=master" > "${COMPONENTS_KUBEADM_GENERATED_FILE}" -echo "Generated ${COMPONENTS_KUBEADM_GENERATED_FILE}" - -# Generate Kubeadm Controlplane components file. -kustomize build "github.com/kubernetes-sigs/cluster-api/controlplane/kubeadm/config/?ref=master" > "${COMPONENTS_CTRLPLANE_GENERATED_FILE}" -echo "Generated ${COMPONENTS_CTRLPLANE_GENERATED_FILE}" - # Generate METAL3 Infrastructure Provider components file. kustomize build "${SOURCE_DIR}/../config" | envsubst > "${COMPONENTS_METAL3_GENERATED_FILE}" echo "Generated ${COMPONENTS_METAL3_GENERATED_FILE}" diff --git a/examples/ippool/ippool.yaml b/examples/ippool/ippool.yaml new file mode 100644 index 00000000..52805433 --- /dev/null +++ b/examples/ippool/ippool.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: ipam.metal3.io/v1alpha1 +kind: IPPool +metadata: + name: pool1 +spec: + clusterName: ${CLUSTER_NAME} + +--- +apiVersion: ipam.metal3.io/v1alpha1 +kind: IPClaim +metadata: + name: Claim1 +spec: + Pool: + Name: pool1 diff --git a/examples/cluster/kustomization.yaml b/examples/ippool/kustomization.yaml similarity index 89% rename from examples/cluster/kustomization.yaml rename to examples/ippool/kustomization.yaml index ee4770ad..93aeb940 100644 --- a/examples/cluster/kustomization.yaml +++ b/examples/ippool/kustomization.yaml @@ -2,6 +2,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: default resources: -- cluster.yaml +- ippool.yaml configurations: - kustomizeconfig.yaml diff --git a/examples/ippool/kustomizeconfig.yaml b/examples/ippool/kustomizeconfig.yaml new file mode 100644 index 00000000..1d541ba0 --- /dev/null +++ b/examples/ippool/kustomizeconfig.yaml @@ -0,0 +1,6 @@ +namespace: +- kind: IPClaim + group: ipam.metal3.io + version: v1alpha1 + path: spec/pool/namespace + create: true diff --git a/examples/machinedeployment/kustomization.yaml b/examples/machinedeployment/kustomization.yaml deleted file mode 100644 index b3c0eecf..00000000 --- a/examples/machinedeployment/kustomization.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -namespace: default -resources: -- machinedeployment.yaml -configurations: -- kustomizeconfig.yaml diff --git a/examples/machinedeployment/kustomizeconfig.yaml b/examples/machinedeployment/kustomizeconfig.yaml deleted file mode 100644 index e0e40917..00000000 --- a/examples/machinedeployment/kustomizeconfig.yaml +++ /dev/null @@ -1,11 +0,0 @@ -namespace: -- kind: MachineDeployment - group: cluster.x-k8s.io - version: v1alpha3 - path: spec/template/spec/infrastructureRef/namespace - create: true -- kind: MachineDeployment - group: cluster.x-k8s.io - version: v1alpha3 - path: spec/template/spec/bootstrap/configRef/namespace - create: true diff --git a/examples/machinedeployment/machinedeployment.yaml b/examples/machinedeployment/machinedeployment.yaml deleted file mode 100644 index 8dfcc2d4..00000000 --- a/examples/machinedeployment/machinedeployment.yaml +++ /dev/null @@ -1,144 +0,0 @@ -apiVersion: cluster.x-k8s.io/v1alpha3 -kind: MachineDeployment -metadata: - name: ${CLUSTER_NAME}-md-0 - labels: - cluster.x-k8s.io/cluster-name: ${CLUSTER_NAME} - nodepool: nodepool-0 -spec: - clusterName: ${CLUSTER_NAME} - replicas: 2 - selector: - matchLabels: - cluster.x-k8s.io/cluster-name: ${CLUSTER_NAME} - nodepool: nodepool-0 - template: - metadata: - labels: - cluster.x-k8s.io/cluster-name: ${CLUSTER_NAME} - nodepool: nodepool-0 - spec: - clusterName: ${CLUSTER_NAME} - version: ${KUBERNETES_VERSION} - bootstrap: - configRef: - name: ${CLUSTER_NAME}-md-0 - apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3 - kind: KubeadmConfigTemplate - infrastructureRef: - name: ${CLUSTER_NAME}-md-0 - apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 - kind: Metal3MachineTemplate ---- -apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 -kind: Metal3MachineTemplate -metadata: - name: ${CLUSTER_NAME}-md-0 -spec: - template: - spec: - image: - url: "http://172.22.0.1/images/rhcos-ootpa-latest.qcow2" - checksum: "97830b21ed272a3d854615beb54cf004" - dataTemplate: - name: ${CLUSTER_NAME}-md-metadata ---- -apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 -kind: Metal3DataTemplate -metadata: - name: ${CLUSTER_NAME}-md-metadata -spec: - clusterName: ${CLUSTER_NAME} - metaData: - strings: - - key: abc - value: def - indexes: - - key: index_0_1 - offset: 0 - step: 1 - - key: index_3 - offset: 3 - - key: index_5_2 - offset: 5 - step: 2 - objectNames: - - key: machine_name - object: machine - - key: metal3machine_name - object: metal3machine - - key: bmh_name - object: baremetalhost - ipAddressesFromPool: - - key: ip_1 - name: pool1 - - key: ip_2 - name: pool2 - - key: ip6_1 - name: pool6-1 - - key: ip6_2 - name: pool6-2 - prefixesFromPool: - - key: prefix_1 - name: pool1 - - key: prefix_2 - name: pool2 - - key: prefix6_1 - name: pool6-1 - - key: prefix6_2 - name: pool6-2 - gatewaysFromPool: - - key: gateway_1 - name: pool1 - - key: gateway_2 - name: pool2 - - key: gateway6_1 - name: pool6-1 - - key: gateway6_2 - name: pool6-2 - fromHostInterfaces: - - key: mac - interface: eth0 - networkData: - links: - ethernets: - - type: phy - id: enp1s0 - macAddress: - fromHostInterface: eth0 - - type: phy - id: enp2s0 - macAddress: - fromHostInterface: eth1 - networks: - ipv4DHCP: - - id: provisioning - link: enp1s0 - ipv4: - - id: baremetal - link: enp2s0 - ipAddressFromIPPool: pool1 - routes: - - network: 0.0.0.0 - prefix: 0 - gateway: - fromIPPool: pool1 - services: - dns: - - 8.8.4.4 - services: - dns: - - 8.8.8.8 ---- -apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3 -kind: KubeadmConfigTemplate -metadata: - name: ${CLUSTER_NAME}-md-0 -spec: - template: - spec: - joinConfiguration: - nodeRegistration: - name: '{{ ds.meta_data.hostname }}' - kubeletExtraArgs: - cloud-provider: baremetal diff --git a/examples/metal3crds/kustomization.yaml b/examples/metal3crds/kustomization.yaml deleted file mode 100644 index 91d70fdb..00000000 --- a/examples/metal3crds/kustomization.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -namespace: default -resources: -- metal3.io_baremetalhosts.yaml diff --git a/examples/metal3crds/metal3.io_baremetalhosts.yaml b/examples/metal3crds/metal3.io_baremetalhosts.yaml deleted file mode 100644 index f69efcad..00000000 --- a/examples/metal3crds/metal3.io_baremetalhosts.yaml +++ /dev/null @@ -1,574 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: baremetalhosts.metal3.io -spec: - additionalPrinterColumns: - - JSONPath: .status.operationalStatus - description: Operational status - name: Status - type: string - - JSONPath: .status.provisioning.state - description: Provisioning status - name: Provisioning Status - type: string - - JSONPath: .spec.consumerRef.name - description: Consumer using this host - name: Consumer - type: string - - JSONPath: .spec.bmc.address - description: Address of management controller - name: BMC - type: string - - JSONPath: .status.hardwareProfile - description: The type of hardware detected - name: Hardware Profile - type: string - - JSONPath: .spec.online - description: Whether the host is online or not - name: Online - type: string - - JSONPath: .status.errorMessage - description: Most recent error - name: Error - type: string - group: metal3.io - names: - kind: BareMetalHost - listKind: BareMetalHostList - plural: baremetalhosts - shortNames: - - bmh - - bmhost - singular: baremetalhost - scope: Namespaced - validation: - openAPIV3Schema: - description: BareMetalHost is the Schema for the baremetalhosts API - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: BareMetalHostSpec defines the desired state of BareMetalHost - properties: - bmc: - description: How do we connect to the BMC? - properties: - address: - description: Address holds the URL for accessing the controller - on the network. - type: string - credentialsName: - description: The name of the secret containing the BMC credentials - (requires keys "username" and "password"). - type: string - disableCertificateVerification: - description: DisableCertificateVerification disables verification - of server certificates when using HTTPS to connect to the BMC. - This is required when the server certificate is self-signed, but - is insecure because it allows a man-in-the-middle to intercept - the connection. - type: boolean - required: - - address - - credentialsName - type: object - bootMACAddress: - description: Which MAC address will PXE boot? This is optional for some - types, but required for libvirt VMs driven by vbmc. - pattern: '[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}' - type: string - consumerRef: - description: ConsumerRef can be used to store information about something - that is using a host. When it is not empty, the host is considered - "in use". - properties: - apiVersion: - description: API version of the referent. - type: string - fieldPath: - description: 'If referring to a piece of an object instead of an - entire object, this string should contain a valid JSON/Go field - access statement, such as desiredState.manifest.containers[2]. - For example, if the object reference is to a container within - a pod, this would take on a value like: "spec.containers{name}" - (where "name" refers to the name of the container that triggered - the event) or if no container name is specified "spec.containers[2]" - (container with index 2 in this pod). This syntax is chosen only - to have some well-defined way of referencing a part of an object. - TODO: this design is not final and this field is subject to change - in the future.' - type: string - kind: - description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - namespace: - description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' - type: string - resourceVersion: - description: 'Specific resourceVersion to which this reference is - made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' - type: string - uid: - description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' - type: string - type: object - description: - description: Description is a human-entered text used to help identify - the host - type: string - externallyProvisioned: - description: ExternallyProvisioned means something else is managing - the image running on the host and the operator should only manage - the power status and hardware inventory inspection. If the Image field - is filled in, this field is ignored. - type: boolean - hardwareProfile: - description: What is the name of the hardware profile for this host? - It should only be necessary to set this when inspection cannot automatically - determine the profile. - type: string - image: - description: Image holds the details of the image to be provisioned. - properties: - checksum: - description: Checksum is the checksum for the image. - type: string - url: - description: URL is a location of an image to deploy. - type: string - required: - - checksum - - url - type: object - metaData: - description: MetaData holds the reference to the Secret containing host - metadata (e.g. meta_data.json which is passed to Config Drive). - properties: - name: - description: Name is unique within a namespace to reference a secret - resource. - type: string - namespace: - description: Namespace defines the space within which the secret - name must be unique. - type: string - type: object - networkData: - description: NetworkData holds the reference to the Secret containing - network configuration (e.g content of network_data.json which is passed - to Config Drive). - properties: - name: - description: Name is unique within a namespace to reference a secret - resource. - type: string - namespace: - description: Namespace defines the space within which the secret - name must be unique. - type: string - type: object - online: - description: Should the server be online? - type: boolean - taints: - description: Taints is the full, authoritative list of taints to apply - to the corresponding Machine. This list will overwrite any modifications - made to the Machine on an ongoing basis. - items: - description: The node this Taint is attached to has the "effect" on - any pod that does not tolerate the Taint. - properties: - effect: - description: Required. The effect of the taint on pods that do - not tolerate the taint. Valid effects are NoSchedule, PreferNoSchedule - and NoExecute. - type: string - key: - description: Required. The taint key to be applied to a node. - type: string - timeAdded: - description: TimeAdded represents the time at which the taint - was added. It is only written for NoExecute taints. - format: date-time - type: string - value: - description: Required. The taint value corresponding to the taint - key. - type: string - required: - - effect - - key - type: object - type: array - userData: - description: UserData holds the reference to the Secret containing the - user data to be passed to the host before it boots. - properties: - name: - description: Name is unique within a namespace to reference a secret - resource. - type: string - namespace: - description: Namespace defines the space within which the secret - name must be unique. - type: string - type: object - required: - - online - type: object - status: - description: BareMetalHostStatus defines the observed state of BareMetalHost - properties: - errorMessage: - description: the last error message reported by the provisioning subsystem - type: string - errorType: - description: ErrorType indicates the type of failure encountered when - the OperationalStatus is OperationalStatusError - enum: - - registration error - - inspection error - - provisioning error - - power management error - type: string - goodCredentials: - description: the last credentials we were able to validate as working - properties: - credentials: - description: SecretReference represents a Secret Reference. It has - enough information to retrieve secret in any namespace - properties: - name: - description: Name is unique within a namespace to reference - a secret resource. - type: string - namespace: - description: Namespace defines the space within which the secret - name must be unique. - type: string - type: object - credentialsVersion: - type: string - type: object - hardware: - description: The hardware discovered to exist on the host. - properties: - cpu: - description: CPU describes one processor on the host. - properties: - arch: - type: string - clockMegahertz: - description: ClockSpeed is a clock speed in MHz - count: - type: integer - flags: - items: - type: string - type: array - model: - type: string - required: - - arch - - clockMegahertz - - count - - flags - - model - type: object - firmware: - description: Firmware describes the firmware on the host. - properties: - bios: - description: The BIOS for this firmware - properties: - date: - description: The release/build date for this BIOS - type: string - vendor: - description: The vendor name for this BIOS - type: string - version: - description: The version of the BIOS - type: string - required: - - date - - vendor - - version - type: object - required: - - bios - type: object - hostname: - type: string - nics: - items: - description: NIC describes one network interface on the host. - properties: - ip: - description: The IP address of the device - type: string - mac: - description: The device MAC addr - pattern: '[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}' - type: string - model: - description: The name of the model, e.g. "virt-io" - type: string - name: - description: The name of the NIC, e.g. "nic-1" - type: string - pxe: - description: Whether the NIC is PXE Bootable - type: boolean - speedGbps: - description: The speed of the device - type: integer - vlanId: - description: The untagged VLAN ID - format: int32 - type: integer - vlans: - description: The VLANs available - items: - description: VLAN represents the name and ID of a VLAN - properties: - id: - description: VLANID is a 12-bit 802.1Q VLAN identifier - format: int32 - type: integer - name: - type: string - required: - - id - type: object - type: array - required: - - ip - - mac - - model - - name - - pxe - - speedGbps - - vlanId - type: object - type: array - ramMebibytes: - type: integer - storage: - items: - description: Storage describes one storage device (disk, SSD, - etc.) on the host. - properties: - hctl: - description: The SCSI location of the device - type: string - model: - description: Hardware model - type: string - name: - description: A name for the disk, e.g. "disk 1 (boot)" - type: string - rotational: - description: Whether this disk represents rotational storage - type: boolean - serialNumber: - description: The serial number of the device - type: string - sizeBytes: - description: The size of the disk in Bytes - format: int64 - type: integer - vendor: - description: The name of the vendor of the device - type: string - wwn: - description: The WWN of the device - type: string - wwnVendorExtension: - description: The WWN Vendor extension of the device - type: string - wwnWithExtension: - description: The WWN with the extension - type: string - required: - - name - - rotational - - serialNumber - - sizeBytes - type: object - type: array - systemVendor: - description: HardwareSystemVendor stores details about the whole - hardware system. - properties: - manufacturer: - type: string - productName: - type: string - serialNumber: - type: string - required: - - manufacturer - - productName - - serialNumber - type: object - required: - - cpu - - firmware - - hostname - - nics - - ramMebibytes - - storage - - systemVendor - type: object - hardwareProfile: - description: The name of the profile matching the hardware details. - type: string - lastUpdated: - description: LastUpdated identifies when this status was last observed. - format: date-time - type: string - operationHistory: - description: OperationHistory holds information about operations performed - on this host. - properties: - deprovision: - description: OperationMetric contains metadata about an operation - (inspection, provisioning, etc.) used for tracking metrics. - properties: - end: - format: date-time - nullable: true - type: string - start: - format: date-time - nullable: true - type: string - type: object - inspect: - description: OperationMetric contains metadata about an operation - (inspection, provisioning, etc.) used for tracking metrics. - properties: - end: - format: date-time - nullable: true - type: string - start: - format: date-time - nullable: true - type: string - type: object - provision: - description: OperationMetric contains metadata about an operation - (inspection, provisioning, etc.) used for tracking metrics. - properties: - end: - format: date-time - nullable: true - type: string - start: - format: date-time - nullable: true - type: string - type: object - register: - description: OperationMetric contains metadata about an operation - (inspection, provisioning, etc.) used for tracking metrics. - properties: - end: - format: date-time - nullable: true - type: string - start: - format: date-time - nullable: true - type: string - type: object - type: object - operationalStatus: - description: OperationalStatus holds the status of the host - enum: - - "" - - OK - - discovered - - error - type: string - poweredOn: - description: indicator for whether or not the host is powered on - type: boolean - provisioning: - description: Information tracked by the provisioner. - properties: - ID: - description: The machine's UUID from the underlying provisioning - tool - type: string - image: - description: Image holds the details of the last image successfully - provisioned to the host. - properties: - checksum: - description: Checksum is the checksum for the image. - type: string - url: - description: URL is a location of an image to deploy. - type: string - required: - - checksum - - url - type: object - state: - description: An indiciator for what the provisioner is doing with - the host. - type: string - required: - - ID - - state - type: object - triedCredentials: - description: the last credentials we sent to the provisioning backend - properties: - credentials: - description: SecretReference represents a Secret Reference. It has - enough information to retrieve secret in any namespace - properties: - name: - description: Name is unique within a namespace to reference - a secret resource. - type: string - namespace: - description: Namespace defines the space within which the secret - name must be unique. - type: string - type: object - credentialsVersion: - type: string - type: object - required: - - errorMessage - - hardwareProfile - - operationHistory - - operationalStatus - - poweredOn - - provisioning - type: object - type: object - version: v1alpha1 - versions: - - name: v1alpha1 - served: true - storage: true diff --git a/examples/metal3plane/hosts.yaml b/examples/metal3plane/hosts.yaml deleted file mode 100644 index 3bbf8906..00000000 --- a/examples/metal3plane/hosts.yaml +++ /dev/null @@ -1,749 +0,0 @@ ---- -apiVersion: v1 -kind: Secret -metadata: - name: demo-externally-provisioned-secret -type: Opaque -data: - username: YWRtaW4= - password: MWYyZDFlMmU2N2Rm ---- -apiVersion: metal3.io/v1alpha1 -kind: BareMetalHost -metadata: - name: metal3-0 -spec: - online: false - bmc: - address: ipmi://192.168.122.10:6233 - credentialsName: demo-externally-provisioned-secret -status: - errorMessage: "" - goodCredentials: - credentials: - name: demo-externally-provisioned-secret - namespace: default - credentialsVersion: "879" - hardware: - cpu: - arch: x86_64 - clockMegahertz: 2494.222 - count: 4 - flags: - - aes - model: Intel Xeon E3-12xx v2 (Ivy Bridge) - firmware: - bios: - date: 04/01/2014 - vendor: SeaBIOS - version: 1.10.2-1ubuntu1 - ramMebibytes: 8192 - storage: - - hctl: "6:0:0:0" - model: QEMU HARDDISK - name: /dev/sda - rotational: true - serialNumber: drive-scsi0-0-0-0 - sizeBytes: 53687091200 - vendor: QEMU - systemVendor: - manufacturer: QEMU - productName: Standard PC (Q35 + ICH9, 2009) - serialNumber: "" - hostname: master-0 - nics: - - ip: 172.22.0.11 - mac: 00:28:19:1f:79:4d - model: 0x1af4 0x0001 - name: eth0 - pxe: true - speedGbps: 0 - vlanId: 0 - - ip: 192.168.111.20 - mac: 00:28:19:1f:79:4f - model: 0x1af4 0x0001 - name: eth1 - pxe: false - speedGbps: 0 - vlanId: 0 - hardwareProfile: unknown - operationalStatus: OK - operationHistory: {} - poweredOn: false - triedCredentials: - credentials: - name: demo-externally-provisioned-secret - namespace: default - credentialsVersion: "879" - provisioning: - ID: be02cfb3-7b24-47c7-b9b8-d69207d86f1e - image: - checksum: http://172.22.0.1/images/centos-updated.qcow2.md5sum - url: http://172.22.0.1/images/centos-updated.qcow2 - state: ready ---- -apiVersion: metal3.io/v1alpha1 -kind: BareMetalHost -metadata: - name: metal3-1 -spec: - online: false - bmc: - address: ipmi://192.168.122.11:6233 - credentialsName: demo-externally-provisioned-secret -status: - errorMessage: "" - goodCredentials: - credentials: - name: demo-externally-provisioned-secret - namespace: default - credentialsVersion: "879" - hardware: - cpu: - arch: x86_64 - clockMegahertz: 2494.222 - count: 4 - flags: - - aes - model: Intel Xeon E3-12xx v2 (Ivy Bridge) - firmware: - bios: - date: 04/01/2014 - vendor: SeaBIOS - version: 1.10.2-1ubuntu1 - ramMebibytes: 8192 - storage: - - hctl: "6:0:0:0" - model: QEMU HARDDISK - name: /dev/sda - rotational: true - serialNumber: drive-scsi0-0-0-0 - sizeBytes: 53687091200 - vendor: QEMU - systemVendor: - manufacturer: QEMU - productName: Standard PC (Q35 + ICH9, 2009) - serialNumber: "" - hostname: master-0 - nics: - - ip: 172.22.0.11 - mac: 00:28:19:1f:79:4d - model: 0x1af4 0x0001 - name: eth0 - pxe: true - speedGbps: 0 - vlanId: 0 - - ip: 192.168.111.20 - mac: 00:28:19:1f:79:4f - model: 0x1af4 0x0001 - name: eth1 - pxe: false - speedGbps: 0 - vlanId: 0 - hardwareProfile: unknown - operationalStatus: OK - operationHistory: {} - poweredOn: false - triedCredentials: - credentials: - name: demo-externally-provisioned-secret - namespace: default - credentialsVersion: "879" - provisioning: - ID: be02cfb3-7b24-47c7-b9b8-d69207d86f10 - image: - checksum: http://172.22.0.1/images/centos-updated.qcow2.md5sum - url: http://172.22.0.1/images/centos-updated.qcow2 - state: ready ---- -apiVersion: metal3.io/v1alpha1 -kind: BareMetalHost -metadata: - name: metal3-2 -spec: - online: false - bmc: - address: ipmi://192.168.122.12:6233 - credentialsName: demo-externally-provisioned-secret -status: - errorMessage: "" - goodCredentials: - credentials: - name: demo-externally-provisioned-secret - namespace: default - credentialsVersion: "879" - hardware: - cpu: - arch: x86_64 - clockMegahertz: 2494.222 - count: 4 - flags: - - aes - model: Intel Xeon E3-12xx v2 (Ivy Bridge) - firmware: - bios: - date: 04/01/2014 - vendor: SeaBIOS - version: 1.10.2-1ubuntu1 - ramMebibytes: 8192 - storage: - - hctl: "6:0:0:0" - model: QEMU HARDDISK - name: /dev/sda - rotational: true - serialNumber: drive-scsi0-0-0-0 - sizeBytes: 53687091200 - vendor: QEMU - systemVendor: - manufacturer: QEMU - productName: Standard PC (Q35 + ICH9, 2009) - serialNumber: "" - hostname: master-0 - nics: - - ip: 172.22.0.11 - mac: 00:28:19:1f:79:4d - model: 0x1af4 0x0001 - name: eth0 - pxe: true - speedGbps: 0 - vlanId: 0 - - ip: 192.168.111.20 - mac: 00:28:19:1f:79:4f - model: 0x1af4 0x0001 - name: eth1 - pxe: false - speedGbps: 0 - vlanId: 0 - hardwareProfile: unknown - operationalStatus: OK - operationHistory: {} - poweredOn: false - triedCredentials: - credentials: - name: demo-externally-provisioned-secret - namespace: default - credentialsVersion: "879" - provisioning: - ID: be02cfb3-7b24-47c7-b9b8-d69207d86f11 - image: - checksum: http://172.22.0.1/images/centos-updated.qcow2.md5sum - url: http://172.22.0.1/images/centos-updated.qcow2 - state: ready ---- -apiVersion: metal3.io/v1alpha1 -kind: BareMetalHost -metadata: - name: metal3-3 -spec: - online: false - bmc: - address: ipmi://192.168.122.13:6233 - credentialsName: demo-externally-provisioned-secret -status: - errorMessage: "" - goodCredentials: - credentials: - name: demo-externally-provisioned-secret - namespace: default - credentialsVersion: "879" - hardware: - cpu: - arch: x86_64 - clockMegahertz: 2494.222 - count: 4 - flags: - - aes - model: Intel Xeon E3-12xx v2 (Ivy Bridge) - firmware: - bios: - date: 04/01/2014 - vendor: SeaBIOS - version: 1.10.2-1ubuntu1 - ramMebibytes: 8192 - storage: - - hctl: "6:0:0:0" - model: QEMU HARDDISK - name: /dev/sda - rotational: true - serialNumber: drive-scsi0-0-0-0 - sizeBytes: 53687091200 - vendor: QEMU - systemVendor: - manufacturer: QEMU - productName: Standard PC (Q35 + ICH9, 2009) - serialNumber: "" - hostname: master-0 - nics: - - ip: 172.22.0.11 - mac: 00:28:19:1f:79:4d - model: 0x1af4 0x0001 - name: eth0 - pxe: true - speedGbps: 0 - vlanId: 0 - - ip: 192.168.111.20 - mac: 00:28:19:1f:79:4f - model: 0x1af4 0x0001 - name: eth1 - pxe: false - speedGbps: 0 - vlanId: 0 - hardwareProfile: unknown - operationalStatus: OK - operationHistory: {} - poweredOn: false - triedCredentials: - credentials: - name: demo-externally-provisioned-secret - namespace: default - credentialsVersion: "879" - provisioning: - ID: be02cfb3-7b24-47c7-b9b8-d69207d86f12 - image: - checksum: http://172.22.0.1/images/centos-updated.qcow2.md5sum - url: http://172.22.0.1/images/centos-updated.qcow2 - state: ready ---- -apiVersion: metal3.io/v1alpha1 -kind: BareMetalHost -metadata: - name: metal3-4 -spec: - online: false - bmc: - address: ipmi://192.168.122.14:6233 - credentialsName: demo-externally-provisioned-secret -status: - errorMessage: "" - goodCredentials: - credentials: - name: demo-externally-provisioned-secret - namespace: default - credentialsVersion: "879" - hardware: - cpu: - arch: x86_64 - clockMegahertz: 2494.222 - count: 4 - flags: - - aes - model: Intel Xeon E3-12xx v2 (Ivy Bridge) - firmware: - bios: - date: 04/01/2014 - vendor: SeaBIOS - version: 1.10.2-1ubuntu1 - ramMebibytes: 8192 - storage: - - hctl: "6:0:0:0" - model: QEMU HARDDISK - name: /dev/sda - rotational: true - serialNumber: drive-scsi0-0-0-0 - sizeBytes: 53687091200 - vendor: QEMU - systemVendor: - manufacturer: QEMU - productName: Standard PC (Q35 + ICH9, 2009) - serialNumber: "" - hostname: master-0 - nics: - - ip: 172.22.0.11 - mac: 00:28:19:1f:79:4d - model: 0x1af4 0x0001 - name: eth0 - pxe: true - speedGbps: 0 - vlanId: 0 - - ip: 192.168.111.20 - mac: 00:28:19:1f:79:4f - model: 0x1af4 0x0001 - name: eth1 - pxe: false - speedGbps: 0 - vlanId: 0 - hardwareProfile: unknown - operationalStatus: OK - operationHistory: {} - poweredOn: false - triedCredentials: - credentials: - name: demo-externally-provisioned-secret - namespace: default - credentialsVersion: "879" - provisioning: - ID: be02cfb3-7b24-47c7-b9b8-d69207d86f13 - image: - checksum: http://172.22.0.1/images/centos-updated.qcow2.md5sum - url: http://172.22.0.1/images/centos-updated.qcow2 - state: ready ---- -apiVersion: metal3.io/v1alpha1 -kind: BareMetalHost -metadata: - name: metal3-5 -spec: - online: false - bmc: - address: ipmi://192.168.122.15:6233 - credentialsName: demo-externally-provisioned-secret -status: - errorMessage: "" - goodCredentials: - credentials: - name: demo-externally-provisioned-secret - namespace: default - credentialsVersion: "879" - hardware: - cpu: - arch: x86_64 - clockMegahertz: 2494.222 - count: 4 - flags: - - aes - model: Intel Xeon E3-12xx v2 (Ivy Bridge) - firmware: - bios: - date: 04/01/2014 - vendor: SeaBIOS - version: 1.10.2-1ubuntu1 - ramMebibytes: 8192 - storage: - - hctl: "6:0:0:0" - model: QEMU HARDDISK - name: /dev/sda - rotational: true - serialNumber: drive-scsi0-0-0-0 - sizeBytes: 53687091200 - vendor: QEMU - systemVendor: - manufacturer: QEMU - productName: Standard PC (Q35 + ICH9, 2009) - serialNumber: "" - hostname: master-0 - nics: - - ip: 172.22.0.11 - mac: 00:28:19:1f:79:4d - model: 0x1af4 0x0001 - name: eth0 - pxe: true - speedGbps: 0 - vlanId: 0 - - ip: 192.168.111.20 - mac: 00:28:19:1f:79:4f - model: 0x1af4 0x0001 - name: eth1 - pxe: false - speedGbps: 0 - vlanId: 0 - hardwareProfile: unknown - operationalStatus: OK - operationHistory: {} - poweredOn: false - triedCredentials: - credentials: - name: demo-externally-provisioned-secret - namespace: default - credentialsVersion: "879" - provisioning: - ID: be02cfb3-7b24-47c7-b9b8-d69207d86f14 - image: - checksum: http://172.22.0.1/images/centos-updated.qcow2.md5sum - url: http://172.22.0.1/images/centos-updated.qcow2 - state: ready ---- -apiVersion: metal3.io/v1alpha1 -kind: BareMetalHost -metadata: - name: metal3-6 -spec: - online: false - bmc: - address: ipmi://192.168.122.16:6233 - credentialsName: demo-externally-provisioned-secret -status: - errorMessage: "" - goodCredentials: - credentials: - name: demo-externally-provisioned-secret - namespace: default - credentialsVersion: "879" - hardware: - cpu: - arch: x86_64 - clockMegahertz: 2494.222 - count: 4 - flags: - - aes - model: Intel Xeon E3-12xx v2 (Ivy Bridge) - firmware: - bios: - date: 04/01/2014 - vendor: SeaBIOS - version: 1.10.2-1ubuntu1 - ramMebibytes: 8192 - storage: - - hctl: "6:0:0:0" - model: QEMU HARDDISK - name: /dev/sda - rotational: true - serialNumber: drive-scsi0-0-0-0 - sizeBytes: 53687091200 - vendor: QEMU - systemVendor: - manufacturer: QEMU - productName: Standard PC (Q35 + ICH9, 2009) - serialNumber: "" - hostname: master-0 - nics: - - ip: 172.22.0.11 - mac: 00:28:19:1f:79:4d - model: 0x1af4 0x0001 - name: eth0 - pxe: true - speedGbps: 0 - vlanId: 0 - - ip: 192.168.111.20 - mac: 00:28:19:1f:79:4f - model: 0x1af4 0x0001 - name: eth1 - pxe: false - speedGbps: 0 - vlanId: 0 - hardwareProfile: unknown - operationalStatus: OK - operationHistory: {} - poweredOn: false - triedCredentials: - credentials: - name: demo-externally-provisioned-secret - namespace: default - credentialsVersion: "879" - provisioning: - ID: be02cfb3-7b24-47c7-b9b8-d69207d86f15 - image: - checksum: http://172.22.0.1/images/centos-updated.qcow2.md5sum - url: http://172.22.0.1/images/centos-updated.qcow2 - state: ready ---- -apiVersion: metal3.io/v1alpha1 -kind: BareMetalHost -metadata: - name: metal3-7 -spec: - online: false - bmc: - address: ipmi://192.168.122.17:6233 - credentialsName: demo-externally-provisioned-secret -status: - errorMessage: "" - goodCredentials: - credentials: - name: demo-externally-provisioned-secret - namespace: default - credentialsVersion: "879" - hardware: - cpu: - arch: x86_64 - clockMegahertz: 2494.222 - count: 4 - flags: - - aes - model: Intel Xeon E3-12xx v2 (Ivy Bridge) - firmware: - bios: - date: 04/01/2014 - vendor: SeaBIOS - version: 1.10.2-1ubuntu1 - ramMebibytes: 8192 - storage: - - hctl: "6:0:0:0" - model: QEMU HARDDISK - name: /dev/sda - rotational: true - serialNumber: drive-scsi0-0-0-0 - sizeBytes: 53687091200 - vendor: QEMU - systemVendor: - manufacturer: QEMU - productName: Standard PC (Q35 + ICH9, 2009) - serialNumber: "" - hostname: master-0 - nics: - - ip: 172.22.0.11 - mac: 00:28:19:1f:79:4d - model: 0x1af4 0x0001 - name: eth0 - pxe: true - speedGbps: 0 - vlanId: 0 - - ip: 192.168.111.20 - mac: 00:28:19:1f:79:4f - model: 0x1af4 0x0001 - name: eth1 - pxe: false - speedGbps: 0 - vlanId: 0 - hardwareProfile: unknown - operationalStatus: OK - operationHistory: {} - poweredOn: false - triedCredentials: - credentials: - name: demo-externally-provisioned-secret - namespace: default - credentialsVersion: "879" - provisioning: - ID: be02cfb3-7b24-47c7-b9b8-d69207d86f16 - image: - checksum: http://172.22.0.1/images/centos-updated.qcow2.md5sum - url: http://172.22.0.1/images/centos-updated.qcow2 - state: ready ---- -apiVersion: metal3.io/v1alpha1 -kind: BareMetalHost -metadata: - name: metal3-8 -spec: - online: false - bmc: - address: ipmi://192.168.122.18:6233 - credentialsName: demo-externally-provisioned-secret -status: - errorMessage: "" - goodCredentials: - credentials: - name: demo-externally-provisioned-secret - namespace: default - credentialsVersion: "879" - hardware: - cpu: - arch: x86_64 - clockMegahertz: 2494.222 - count: 4 - flags: - - aes - model: Intel Xeon E3-12xx v2 (Ivy Bridge) - firmware: - bios: - date: 04/01/2014 - vendor: SeaBIOS - version: 1.10.2-1ubuntu1 - ramMebibytes: 8192 - storage: - - hctl: "6:0:0:0" - model: QEMU HARDDISK - name: /dev/sda - rotational: true - serialNumber: drive-scsi0-0-0-0 - sizeBytes: 53687091200 - vendor: QEMU - systemVendor: - manufacturer: QEMU - productName: Standard PC (Q35 + ICH9, 2009) - serialNumber: "" - hostname: master-0 - nics: - - ip: 172.22.0.11 - mac: 00:28:19:1f:79:4d - model: 0x1af4 0x0001 - name: eth0 - pxe: true - speedGbps: 0 - vlanId: 0 - - ip: 192.168.111.20 - mac: 00:28:19:1f:79:4f - model: 0x1af4 0x0001 - name: eth1 - pxe: false - speedGbps: 0 - vlanId: 0 - hardwareProfile: unknown - operationalStatus: OK - operationHistory: {} - poweredOn: false - triedCredentials: - credentials: - name: demo-externally-provisioned-secret - namespace: default - credentialsVersion: "879" - provisioning: - ID: be02cfb3-7b24-47c7-b9b8-d69207d86f17 - image: - checksum: http://172.22.0.1/images/centos-updated.qcow2.md5sum - url: http://172.22.0.1/images/centos-updated.qcow2 - state: ready ---- -apiVersion: metal3.io/v1alpha1 -kind: BareMetalHost -metadata: - name: metal3-9 -spec: - online: false - bmc: - address: ipmi://192.168.122.19:6233 - credentialsName: demo-externally-provisioned-secret -status: - errorMessage: "" - goodCredentials: - credentials: - name: demo-externally-provisioned-secret - namespace: default - credentialsVersion: "879" - hardware: - cpu: - arch: x86_64 - clockMegahertz: 2494.222 - count: 4 - flags: - - aes - model: Intel Xeon E3-12xx v2 (Ivy Bridge) - firmware: - bios: - date: 04/01/2014 - vendor: SeaBIOS - version: 1.10.2-1ubuntu1 - ramMebibytes: 8192 - storage: - - hctl: "6:0:0:0" - model: QEMU HARDDISK - name: /dev/sda - rotational: true - serialNumber: drive-scsi0-0-0-0 - sizeBytes: 53687091200 - vendor: QEMU - systemVendor: - manufacturer: QEMU - productName: Standard PC (Q35 + ICH9, 2009) - serialNumber: "" - hostname: master-0 - nics: - - ip: 172.22.0.11 - mac: 00:28:19:1f:79:4d - model: 0x1af4 0x0001 - name: eth0 - pxe: true - speedGbps: 0 - vlanId: 0 - - ip: 192.168.111.20 - mac: 00:28:19:1f:79:4f - model: 0x1af4 0x0001 - name: eth1 - pxe: false - speedGbps: 0 - vlanId: 0 - hardwareProfile: unknown - operationalStatus: OK - operationHistory: {} - poweredOn: false - triedCredentials: - credentials: - name: demo-externally-provisioned-secret - namespace: default - credentialsVersion: "879" - provisioning: - ID: be02cfb3-7b24-47c7-b9b8-d69207d86f18 - image: - checksum: http://172.22.0.1/images/centos-updated.qcow2.md5sum - url: http://172.22.0.1/images/centos-updated.qcow2 - state: ready diff --git a/examples/metal3plane/kustomization.yaml b/examples/metal3plane/kustomization.yaml deleted file mode 100644 index fef80721..00000000 --- a/examples/metal3plane/kustomization.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -namespace: default -resources: -- hosts.yaml diff --git a/examples/provider-components/kustomization.yaml b/examples/provider-components/kustomization.yaml index b93bb2c6..94ef23ef 100644 --- a/examples/provider-components/kustomization.yaml +++ b/examples/provider-components/kustomization.yaml @@ -1,9 +1,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: -- core-components.yaml -- bootstrap-components.yaml -- ctlplane-components.yaml - infrastructure-components.yaml patchesStrategicMerge: - manager_tolerations_patch.yaml diff --git a/examples/provider-components/manager_tolerations_patch.yaml b/examples/provider-components/manager_tolerations_patch.yaml index 5d80c243..05f06e96 100644 --- a/examples/provider-components/manager_tolerations_patch.yaml +++ b/examples/provider-components/manager_tolerations_patch.yaml @@ -2,7 +2,7 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: capm3-controller-manager + name: metal3-ipam-controller-manager namespace: capm3-system spec: template: @@ -12,45 +12,3 @@ spec: key: node-role.kubernetes.io/master - key: CriticalAddonsOnly operator: Exists ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: capi-controller-manager - namespace: capi-system -spec: - template: - spec: - tolerations: - - effect: NoSchedule - key: node-role.kubernetes.io/master - - key: CriticalAddonsOnly - operator: Exists ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: capi-kubeadm-bootstrap-controller-manager - namespace: capi-kubeadm-bootstrap-system -spec: - template: - spec: - tolerations: - - effect: NoSchedule - key: node-role.kubernetes.io/master - - key: CriticalAddonsOnly - operator: Exists ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: capi-kubeadm-control-plane-controller-manager - namespace: capi-kubeadm-control-plane-system -spec: - template: - spec: - tolerations: - - effect: NoSchedule - key: node-role.kubernetes.io/master - - key: CriticalAddonsOnly - operator: Exists diff --git a/ipam/metal3ippool_manager.go b/ipam/ippool_manager.go similarity index 88% rename from ipam/metal3ippool_manager.go rename to ipam/ippool_manager.go index 1338f530..705ced30 100644 --- a/ipam/metal3ippool_manager.go +++ b/ipam/ippool_manager.go @@ -19,9 +19,9 @@ package ipam import ( "context" "fmt" - "strings" - "net" "math/big" + "net" + "strings" "github.com/go-logr/logr" ipamv1 "github.com/metal3-io/ipam/api/v1alpha1" @@ -46,13 +46,13 @@ type IPPoolManagerInterface interface { // IPPoolManager is responsible for performing machine reconciliation type IPPoolManager struct { client client.Client - IPPool *ipamv1.Metal3IPPool + IPPool *ipamv1.IPPool Log logr.Logger } // NewIPPoolManager returns a new helper for managing a ipPool object func NewIPPoolManager(client client.Client, - ipPool *ipamv1.Metal3IPPool, ipPoolLog logr.Logger) (*IPPoolManager, error) { + ipPool *ipamv1.IPPool, ipPoolLog logr.Logger) (*IPPoolManager, error) { return &IPPoolManager{ client: client, @@ -105,7 +105,7 @@ func (m *IPPoolManager) SetClusterOwnerRef(cluster *capi.Cluster) error { // RecreateStatus recreates the status if empty func (m *IPPoolManager) getIndexes(ctx context.Context) (map[ipamv1.IPAddressStr]string, error) { - m.Log.Info("Fetching Metal3IPAddress objects") + m.Log.Info("Fetching IPAddress objects") //start from empty maps m.IPPool.Status.Allocations = make(map[string]ipamv1.IPAddressStr) @@ -116,8 +116,8 @@ func (m *IPPoolManager) getIndexes(ctx context.Context) (map[ipamv1.IPAddressStr addresses[address] = "" } - // get list of Metal3IPAddress objects - addressObjects := ipamv1.Metal3IPAddressList{} + // get list of IPAddress objects + addressObjects := ipamv1.IPAddressList{} // without this ListOption, all namespaces would be including in the listing opts := &client.ListOptions{ Namespace: m.IPPool.Namespace, @@ -128,7 +128,7 @@ func (m *IPPoolManager) getIndexes(ctx context.Context) (map[ipamv1.IPAddressStr return addresses, err } - // Iterate over the Metal3IPAddress objects to find all addresses and objects + // Iterate over the IPAddress objects to find all addresses and objects for _, addressObject := range addressObjects.Items { // If IPPool does not point to this object, discard @@ -157,7 +157,7 @@ func (m *IPPoolManager) updateStatusTimestamp() { m.IPPool.Status.LastUpdated = &now } -// UpdateAddresses manages the claims and creates or deletes Metal3IPAddress accordingly. +// UpdateAddresses manages the claims and creates or deletes IPAddress accordingly. // It returns the number of current allocations func (m *IPPoolManager) UpdateAddresses(ctx context.Context) (int, error) { @@ -166,8 +166,8 @@ func (m *IPPoolManager) UpdateAddresses(ctx context.Context) (int, error) { return 0, err } - // get list of Metal3IPClaim objects - addressClaimObjects := ipamv1.Metal3IPClaimList{} + // get list of IPClaim objects + addressClaimObjects := ipamv1.IPClaimList{} // without this ListOption, all namespaces would be including in the listing opts := &client.ListOptions{ Namespace: m.IPPool.Namespace, @@ -178,7 +178,7 @@ func (m *IPPoolManager) UpdateAddresses(ctx context.Context) (int, error) { return 0, err } - // Iterate over the Metal3IPClaim objects to find all addresses and objects + // Iterate over the IPClaim objects to find all addresses and objects for _, addressClaim := range addressClaimObjects.Items { // If IPPool does not point to this object, discard if addressClaim.Spec.Pool.Name != m.IPPool.Name { @@ -198,7 +198,7 @@ func (m *IPPoolManager) UpdateAddresses(ctx context.Context) (int, error) { } func (m *IPPoolManager) updateAddress(ctx context.Context, - addressClaim *ipamv1.Metal3IPClaim, addresses map[ipamv1.IPAddressStr]string, + addressClaim *ipamv1.IPClaim, addresses map[ipamv1.IPAddressStr]string, ) (map[ipamv1.IPAddressStr]string, error) { helper, err := patch.NewHelper(addressClaim, m.client) if err != nil { @@ -229,7 +229,7 @@ func (m *IPPoolManager) updateAddress(ctx context.Context, return addresses, nil } -func (m *IPPoolManager) allocateAddress(addressClaim *ipamv1.Metal3IPClaim, +func (m *IPPoolManager) allocateAddress(addressClaim *ipamv1.IPClaim, addresses map[ipamv1.IPAddressStr]string, ) (ipamv1.IPAddressStr, int, *ipamv1.IPAddressStr, error) { var err error @@ -271,7 +271,7 @@ func (m *IPPoolManager) allocateAddress(addressClaim *ipamv1.Metal3IPClaim, } func (m *IPPoolManager) createAddress(ctx context.Context, - addressClaim *ipamv1.Metal3IPClaim, addresses map[ipamv1.IPAddressStr]string, + addressClaim *ipamv1.IPClaim, addresses map[ipamv1.IPAddressStr]string, ) (map[ipamv1.IPAddressStr]string, error) { if !Contains(addressClaim.Finalizers, ipamv1.IPClaimFinalizer) { addressClaim.Finalizers = append(addressClaim.Finalizers, @@ -301,16 +301,16 @@ func (m *IPPoolManager) createAddress(ctx context.Context, strings.Replace(string(allocatedAddress), ":", "-", -1), ".", "-", -1, ) - // Set the index and Metal3IPAddress names + // Set the index and IPAddress names addressName := m.IPPool.Spec.NamePrefix + "-" + formatedAddress m.Log.Info("Address allocated", "Claim", addressClaim.Name, "address", allocatedAddress) - // Create the Metal3IPAddress object, with an Owner ref to the Metal3Machine - // (curOwnerRef) and to the Metal3IPPool - addressObject := &ipamv1.Metal3IPAddress{ + // Create the IPAddress object, with an Owner ref to the Metal3Machine + // (curOwnerRef) and to the IPPool + addressObject := &ipamv1.IPAddress{ TypeMeta: metav1.TypeMeta{ - Kind: "Metal3IPAddress", + Kind: "IPAddress", APIVersion: ipamv1.GroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ @@ -333,7 +333,7 @@ func (m *IPPoolManager) createAddress(ctx context.Context, }, Labels: addressClaim.Labels, }, - Spec: ipamv1.Metal3IPAddressSpec{ + Spec: ipamv1.IPAddressSpec{ Address: allocatedAddress, Pool: corev1.ObjectReference{ Name: m.IPPool.Name, @@ -348,12 +348,12 @@ func (m *IPPoolManager) createAddress(ctx context.Context, }, } - // Create the Metal3IPAddress object. If we get a conflict (that will set + // Create the IPAddress object. If we get a conflict (that will set // HasRequeueAfterError), then requeue to retrigger the reconciliation with // the new state if err := createObject(m.client, ctx, addressObject); err != nil { if _, ok := err.(*RequeueAfterError); !ok { - addressClaim.Status.ErrorMessage = pointer.StringPtr("Failed to create associated Metal3IPAddress object") + addressClaim.Status.ErrorMessage = pointer.StringPtr("Failed to create associated IPAddress object") } return addresses, err } @@ -371,15 +371,15 @@ func (m *IPPoolManager) createAddress(ctx context.Context, // DeleteDatas deletes old secrets func (m *IPPoolManager) deleteAddress(ctx context.Context, - addressClaim *ipamv1.Metal3IPClaim, addresses map[ipamv1.IPAddressStr]string, + addressClaim *ipamv1.IPClaim, addresses map[ipamv1.IPAddressStr]string, ) (map[ipamv1.IPAddressStr]string, error) { - m.Log.Info("Deleting Claim", "Metal3IPClaim", addressClaim.Name) + m.Log.Info("Deleting Claim", "IPClaim", addressClaim.Name) allocatedAddress, ok := m.IPPool.Status.Allocations[addressClaim.Name] if ok { - // Try to get the Metal3IPAddress. if it succeeds, delete it - tmpM3Data := &ipamv1.Metal3IPAddress{} + // Try to get the IPAddress. if it succeeds, delete it + tmpM3Data := &ipamv1.IPAddress{} formatedAddress := strings.Replace( strings.Replace(string(allocatedAddress), ":", "-", -1), ".", "-", -1, ) @@ -389,13 +389,13 @@ func (m *IPPoolManager) deleteAddress(ctx context.Context, } err := m.client.Get(ctx, key, tmpM3Data) if err != nil && !apierrors.IsNotFound(err) { - addressClaim.Status.ErrorMessage = pointer.StringPtr("Failed to get associated Metal3IPAddress object") + addressClaim.Status.ErrorMessage = pointer.StringPtr("Failed to get associated IPAddress object") return addresses, err } else if err == nil { // Delete the secret with metadata - err = m.client.Delete(ctx, tmpM3Data) - if err != nil && !apierrors.IsNotFound(err) { - addressClaim.Status.ErrorMessage = pointer.StringPtr("Failed to delete associated Metal3IPAddress object") + err = deleteObject(m.client, ctx, tmpM3Data) + if err != nil { + addressClaim.Status.ErrorMessage = pointer.StringPtr("Failed to delete associated IPAddress object") return addresses, err } } @@ -406,7 +406,7 @@ func (m *IPPoolManager) deleteAddress(ctx context.Context, ipamv1.IPClaimFinalizer, ) - m.Log.Info("Deleted Claim", "Metal3IPClaim", addressClaim.Name) + m.Log.Info("Deleted Claim", "IPClaim", addressClaim.Name) if ok { if _, ok := m.IPPool.Spec.PreAllocations[addressClaim.Name]; !ok { @@ -420,7 +420,7 @@ func (m *IPPoolManager) deleteAddress(ctx context.Context, // getIPAddress renders the IP address, taking the index, offset and step into // account, it is IP version agnostic -func getIPAddress(entry ipamv1.IPPool, index int) (ipamv1.IPAddressStr, error) { +func getIPAddress(entry ipamv1.Pool, index int) (ipamv1.IPAddressStr, error) { if entry.Start == nil && entry.Subnet == nil { return "", errors.New("Either Start or Subnet is required for ipAddress") diff --git a/ipam/metal3ippool_manager_test.go b/ipam/ippool_manager_test.go similarity index 84% rename from ipam/metal3ippool_manager_test.go rename to ipam/ippool_manager_test.go index d9e1eb93..7fa2e99f 100644 --- a/ipam/metal3ippool_manager_test.go +++ b/ipam/ippool_manager_test.go @@ -46,9 +46,9 @@ var ( } ) -var _ = Describe("Metal3IPPool manager", func() { +var _ = Describe("IPPool manager", func() { DescribeTable("Test Finalizers", - func(ipPool *ipamv1.Metal3IPPool) { + func(ipPool *ipamv1.IPPool) { ipPoolMgr, err := NewIPPoolManager(nil, ipPool, klogr.New(), ) @@ -66,8 +66,8 @@ var _ = Describe("Metal3IPPool manager", func() { ipamv1.IPPoolFinalizer, )) }, - Entry("No finalizers", &ipamv1.Metal3IPPool{}), - Entry("Additional Finalizers", &ipamv1.Metal3IPPool{ + Entry("No finalizers", &ipamv1.IPPool{}), + Entry("Additional Finalizers", &ipamv1.IPPool{ ObjectMeta: metav1.ObjectMeta{ Finalizers: []string{"foo"}, }, @@ -76,7 +76,7 @@ var _ = Describe("Metal3IPPool manager", func() { type testCaseSetClusterOwnerRef struct { cluster *capi.Cluster - ipPool *ipamv1.Metal3IPPool + ipPool *ipamv1.IPPool expectError bool } @@ -100,7 +100,7 @@ var _ = Describe("Metal3IPPool manager", func() { expectError: true, }), Entry("no previous ownerref", testCaseSetClusterOwnerRef{ - ipPool: &ipamv1.Metal3IPPool{ + ipPool: &ipamv1.IPPool{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", }, @@ -112,7 +112,7 @@ var _ = Describe("Metal3IPPool manager", func() { }, }), Entry("previous ownerref", testCaseSetClusterOwnerRef{ - ipPool: &ipamv1.Metal3IPPool{ + ipPool: &ipamv1.IPPool{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", OwnerReferences: []metav1.OwnerReference{ @@ -129,7 +129,7 @@ var _ = Describe("Metal3IPPool manager", func() { }, }), Entry("ownerref present", testCaseSetClusterOwnerRef{ - ipPool: &ipamv1.Metal3IPPool{ + ipPool: &ipamv1.IPPool{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", OwnerReferences: []metav1.OwnerReference{ @@ -151,8 +151,8 @@ var _ = Describe("Metal3IPPool manager", func() { ) type testGetIndexes struct { - ipPool *ipamv1.Metal3IPPool - addresses []*ipamv1.Metal3IPAddress + ipPool *ipamv1.IPPool + addresses []*ipamv1.IPAddress expectError bool expectedAddresses map[ipamv1.IPAddressStr]string expectedAllocations map[string]ipamv1.IPAddressStr @@ -181,37 +181,37 @@ var _ = Describe("Metal3IPPool manager", func() { Expect(tc.ipPool.Status.LastUpdated.IsZero()).To(BeFalse()) }, Entry("No addresses", testGetIndexes{ - ipPool: &ipamv1.Metal3IPPool{}, + ipPool: &ipamv1.IPPool{}, expectedAddresses: map[ipamv1.IPAddressStr]string{}, expectedAllocations: map[string]ipamv1.IPAddressStr{}, }), Entry("addresses", testGetIndexes{ - ipPool: &ipamv1.Metal3IPPool{ + ipPool: &ipamv1.IPPool{ ObjectMeta: testObjectMeta, - Spec: ipamv1.Metal3IPPoolSpec{ + Spec: ipamv1.IPPoolSpec{ PreAllocations: map[string]ipamv1.IPAddressStr{ "bcd": ipamv1.IPAddressStr("bcde"), }, }, }, - addresses: []*ipamv1.Metal3IPAddress{ - &ipamv1.Metal3IPAddress{ + addresses: []*ipamv1.IPAddress{ + &ipamv1.IPAddress{ ObjectMeta: metav1.ObjectMeta{ Name: "abc-0", Namespace: "myns", }, - Spec: ipamv1.Metal3IPAddressSpec{ + Spec: ipamv1.IPAddressSpec{ Address: "abcd1", Pool: *testObjectReference, Claim: *testObjectReference, }, }, - &ipamv1.Metal3IPAddress{ + &ipamv1.IPAddress{ ObjectMeta: metav1.ObjectMeta{ Name: "bbc-1", Namespace: "myns", }, - Spec: ipamv1.Metal3IPAddressSpec{ + Spec: ipamv1.IPAddressSpec{ Address: "abcd2", Pool: corev1.ObjectReference{ Name: "bbc", @@ -223,23 +223,23 @@ var _ = Describe("Metal3IPPool manager", func() { }, }, }, - &ipamv1.Metal3IPAddress{ + &ipamv1.IPAddress{ ObjectMeta: metav1.ObjectMeta{ Name: "abc-2", Namespace: "myns", }, - Spec: ipamv1.Metal3IPAddressSpec{ + Spec: ipamv1.IPAddressSpec{ Address: "abcd3", Pool: corev1.ObjectReference{}, Claim: *testObjectReference, }, }, - &ipamv1.Metal3IPAddress{ + &ipamv1.IPAddress{ ObjectMeta: metav1.ObjectMeta{ Name: "abc-3", Namespace: "myns", }, - Spec: ipamv1.Metal3IPAddressSpec{ + Spec: ipamv1.IPAddressSpec{ Address: "abcd4", Pool: corev1.ObjectReference{ Namespace: "myns", @@ -264,9 +264,9 @@ var _ = Describe("Metal3IPPool manager", func() { } type testCaseUpdateAddresses struct { - ipPool *ipamv1.Metal3IPPool - ipClaims []*ipamv1.Metal3IPClaim - ipAddresses []*ipamv1.Metal3IPAddress + ipPool *ipamv1.IPPool + ipClaims []*ipamv1.IPClaim + ipAddresses []*ipamv1.IPAddress expectRequeue bool expectError bool expectedNbAllocations int @@ -303,13 +303,13 @@ var _ = Describe("Metal3IPPool manager", func() { Expect(tc.ipPool.Status.LastUpdated.IsZero()).To(BeFalse()) Expect(tc.ipPool.Status.Allocations).To(Equal(tc.expectedAllocations)) - // get list of Metal3IPAddress objects - addressObjects := ipamv1.Metal3IPClaimList{} + // get list of IPAddress objects + addressObjects := ipamv1.IPClaimList{} opts := &client.ListOptions{} err = c.List(context.TODO(), &addressObjects, opts) Expect(err).NotTo(HaveOccurred()) - // Iterate over the Metal3IPAddress objects to find all indexes and objects + // Iterate over the IPAddress objects to find all indexes and objects for _, claim := range addressObjects.Items { if claim.DeletionTimestamp.IsZero() { fmt.Printf("%#v", claim) @@ -319,80 +319,80 @@ var _ = Describe("Metal3IPPool manager", func() { }, Entry("No Claims", testCaseUpdateAddresses{ - ipPool: &ipamv1.Metal3IPPool{ + ipPool: &ipamv1.IPPool{ ObjectMeta: ipPoolMeta, }, expectedAllocations: map[string]ipamv1.IPAddressStr{}, }), Entry("Claim and IP exist", testCaseUpdateAddresses{ - ipPool: &ipamv1.Metal3IPPool{ + ipPool: &ipamv1.IPPool{ ObjectMeta: ipPoolMeta, - Spec: ipamv1.Metal3IPPoolSpec{ + Spec: ipamv1.IPPoolSpec{ NamePrefix: "abcpref", }, }, - ipClaims: []*ipamv1.Metal3IPClaim{ - &ipamv1.Metal3IPClaim{ + ipClaims: []*ipamv1.IPClaim{ + &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: "myns", }, - Spec: ipamv1.Metal3IPClaimSpec{ + Spec: ipamv1.IPClaimSpec{ Pool: corev1.ObjectReference{ Name: "abc", Namespace: "myns", }, }, }, - &ipamv1.Metal3IPClaim{ + &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abcd", Namespace: "myns", }, - Spec: ipamv1.Metal3IPClaimSpec{ + Spec: ipamv1.IPClaimSpec{ Pool: corev1.ObjectReference{ Name: "abcd", Namespace: "myns", }, }, - Status: ipamv1.Metal3IPClaimStatus{ + Status: ipamv1.IPClaimStatus{ Address: &corev1.ObjectReference{ Name: "abcpref-192-168-1-12", Namespace: "myns", }, }, }, - &ipamv1.Metal3IPClaim{ + &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abce", Namespace: "myns", }, - Spec: ipamv1.Metal3IPClaimSpec{ + Spec: ipamv1.IPClaimSpec{ Pool: corev1.ObjectReference{ Name: "abc", Namespace: "myns", }, }, - Status: ipamv1.Metal3IPClaimStatus{ + Status: ipamv1.IPClaimStatus{ Address: &corev1.ObjectReference{ Name: "abcpref-192-168-1-12", Namespace: "myns", }, }, }, - &ipamv1.Metal3IPClaim{ + &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abcf", Namespace: "myns", DeletionTimestamp: &timeNow, }, - Spec: ipamv1.Metal3IPClaimSpec{ + Spec: ipamv1.IPClaimSpec{ Pool: corev1.ObjectReference{ Name: "abc", Namespace: "myns", }, }, - Status: ipamv1.Metal3IPClaimStatus{ + Status: ipamv1.IPClaimStatus{ Address: &corev1.ObjectReference{ Name: "abcpref-192-168-1-13", Namespace: "myns", @@ -400,13 +400,13 @@ var _ = Describe("Metal3IPPool manager", func() { }, }, }, - ipAddresses: []*ipamv1.Metal3IPAddress{ - &ipamv1.Metal3IPAddress{ + ipAddresses: []*ipamv1.IPAddress{ + &ipamv1.IPAddress{ ObjectMeta: metav1.ObjectMeta{ Name: "abcpref-192-168-1-11", Namespace: "myns", }, - Spec: ipamv1.Metal3IPAddressSpec{ + Spec: ipamv1.IPAddressSpec{ Pool: corev1.ObjectReference{ Name: "abc", Namespace: "myns", @@ -420,12 +420,12 @@ var _ = Describe("Metal3IPPool manager", func() { Prefix: 24, }, }, - &ipamv1.Metal3IPAddress{ + &ipamv1.IPAddress{ ObjectMeta: metav1.ObjectMeta{ Name: "abcpref-192-168-1-12", Namespace: "myns", }, - Spec: ipamv1.Metal3IPAddressSpec{ + Spec: ipamv1.IPAddressSpec{ Pool: corev1.ObjectReference{ Name: "abc", Namespace: "myns", @@ -437,12 +437,12 @@ var _ = Describe("Metal3IPPool manager", func() { Address: ipamv1.IPAddressStr("192.168.1.12"), }, }, - &ipamv1.Metal3IPAddress{ + &ipamv1.IPAddress{ ObjectMeta: metav1.ObjectMeta{ Name: "abcpref-192-168-1-13", Namespace: "myns", }, - Spec: ipamv1.Metal3IPAddressSpec{ + Spec: ipamv1.IPAddressSpec{ Pool: corev1.ObjectReference{ Name: "abc", Namespace: "myns", @@ -464,9 +464,9 @@ var _ = Describe("Metal3IPPool manager", func() { ) type testCaseCreateAddresses struct { - ipPool *ipamv1.Metal3IPPool - ipClaim *ipamv1.Metal3IPClaim - ipAddresses []*ipamv1.Metal3IPAddress + ipPool *ipamv1.IPPool + ipClaim *ipamv1.IPClaim + ipAddresses []*ipamv1.IPAddress addresses map[ipamv1.IPAddressStr]string expectRequeue bool expectError bool @@ -500,14 +500,14 @@ var _ = Describe("Metal3IPPool manager", func() { } else { Expect(err).NotTo(HaveOccurred()) } - // get list of Metal3IPAddress objects - addressObjects := ipamv1.Metal3IPAddressList{} + // get list of IPAddress objects + addressObjects := ipamv1.IPAddressList{} opts := &client.ListOptions{} err = c.List(context.TODO(), &addressObjects, opts) Expect(err).NotTo(HaveOccurred()) Expect(len(tc.expectedIPAddresses)).To(Equal(len(addressObjects.Items))) - // Iterate over the Metal3IPAddress objects to find all indexes and objects + // Iterate over the IPAddress objects to find all indexes and objects for _, address := range addressObjects.Items { Expect(tc.expectedIPAddresses).To(ContainElement(address.Name)) // TODO add further testing later @@ -518,15 +518,15 @@ var _ = Describe("Metal3IPPool manager", func() { Expect(tc.ipPool.Status.Allocations).To(Equal(tc.expectedAllocations)) }, Entry("Already exists", testCaseCreateAddresses{ - ipPool: &ipamv1.Metal3IPPool{ + ipPool: &ipamv1.IPPool{ ObjectMeta: ipPoolMeta, - Status: ipamv1.Metal3IPPoolStatus{ + Status: ipamv1.IPPoolStatus{ Allocations: map[string]ipamv1.IPAddressStr{ "abc": ipamv1.IPAddressStr("foo-0"), }, }, }, - ipClaim: &ipamv1.Metal3IPClaim{ + ipClaim: &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", }, @@ -536,11 +536,11 @@ var _ = Describe("Metal3IPPool manager", func() { }, }), Entry("Not allocated yet, pre-allocated", testCaseCreateAddresses{ - ipPool: &ipamv1.Metal3IPPool{ + ipPool: &ipamv1.IPPool{ ObjectMeta: ipPoolMeta, - Spec: ipamv1.Metal3IPPoolSpec{ - Pools: []ipamv1.IPPool{ - ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + ipamv1.Pool{ Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.11")), End: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.20")), }, @@ -550,12 +550,12 @@ var _ = Describe("Metal3IPPool manager", func() { }, NamePrefix: "abcpref", }, - Status: ipamv1.Metal3IPPoolStatus{ + Status: ipamv1.IPPoolStatus{ Allocations: map[string]ipamv1.IPAddressStr{}, }, }, addresses: map[ipamv1.IPAddressStr]string{}, - ipClaim: &ipamv1.Metal3IPClaim{ + ipClaim: &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", }, @@ -569,25 +569,25 @@ var _ = Describe("Metal3IPPool manager", func() { expectedIPAddresses: []string{"abcpref-192-168-0-15"}, }), Entry("Not allocated yet", testCaseCreateAddresses{ - ipPool: &ipamv1.Metal3IPPool{ + ipPool: &ipamv1.IPPool{ ObjectMeta: ipPoolMeta, - Spec: ipamv1.Metal3IPPoolSpec{ - Pools: []ipamv1.IPPool{ - ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + ipamv1.Pool{ Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.11")), End: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.20")), }, }, NamePrefix: "abcpref", }, - Status: ipamv1.Metal3IPPoolStatus{ + Status: ipamv1.IPPoolStatus{ Allocations: map[string]ipamv1.IPAddressStr{}, }, }, addresses: map[ipamv1.IPAddressStr]string{ ipamv1.IPAddressStr("192.168.0.11"): "bcd", }, - ipClaim: &ipamv1.Metal3IPClaim{ + ipClaim: &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", }, @@ -602,34 +602,34 @@ var _ = Describe("Metal3IPPool manager", func() { expectedIPAddresses: []string{"abcpref-192-168-0-12"}, }), Entry("Not allocated yet, conflict", testCaseCreateAddresses{ - ipPool: &ipamv1.Metal3IPPool{ + ipPool: &ipamv1.IPPool{ ObjectMeta: ipPoolMeta, - Spec: ipamv1.Metal3IPPoolSpec{ - Pools: []ipamv1.IPPool{ - ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + ipamv1.Pool{ Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.11")), End: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.20")), }, }, NamePrefix: "abcpref", }, - Status: ipamv1.Metal3IPPoolStatus{ + Status: ipamv1.IPPoolStatus{ Allocations: map[string]ipamv1.IPAddressStr{}, }, }, addresses: map[ipamv1.IPAddressStr]string{}, - ipClaim: &ipamv1.Metal3IPClaim{ + ipClaim: &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", }, }, - ipAddresses: []*ipamv1.Metal3IPAddress{ - &ipamv1.Metal3IPAddress{ + ipAddresses: []*ipamv1.IPAddress{ + &ipamv1.IPAddress{ ObjectMeta: metav1.ObjectMeta{ Name: "abcpref-192-168-0-11", Namespace: "myns", }, - Spec: ipamv1.Metal3IPAddressSpec{ + Spec: ipamv1.IPAddressSpec{ Address: "192.168.0.11", Pool: corev1.ObjectReference{ Name: "abc", @@ -646,25 +646,25 @@ var _ = Describe("Metal3IPPool manager", func() { expectRequeue: true, }), Entry("Not allocated yet, exhausted pool", testCaseCreateAddresses{ - ipPool: &ipamv1.Metal3IPPool{ + ipPool: &ipamv1.IPPool{ ObjectMeta: ipPoolMeta, - Spec: ipamv1.Metal3IPPoolSpec{ - Pools: []ipamv1.IPPool{ - ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + ipamv1.Pool{ Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.11")), End: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.11")), }, }, NamePrefix: "abcpref", }, - Status: ipamv1.Metal3IPPoolStatus{ + Status: ipamv1.IPPoolStatus{ Allocations: map[string]ipamv1.IPAddressStr{}, }, }, addresses: map[ipamv1.IPAddressStr]string{ ipamv1.IPAddressStr("192.168.0.11"): "bcd", }, - ipClaim: &ipamv1.Metal3IPClaim{ + ipClaim: &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", }, @@ -679,8 +679,8 @@ var _ = Describe("Metal3IPPool manager", func() { ) type testCaseAllocateAddress struct { - ipPool *ipamv1.Metal3IPPool - ipClaim *ipamv1.Metal3IPClaim + ipPool *ipamv1.IPPool + ipClaim *ipamv1.IPClaim addresses map[ipamv1.IPAddressStr]string expectedAddress ipamv1.IPAddressStr expectedPrefix int @@ -708,10 +708,10 @@ var _ = Describe("Metal3IPPool manager", func() { Expect(*gateway).To(Equal(*tc.expectedGateway)) }, Entry("Empty pools", testCaseAllocateAddress{ - ipPool: &ipamv1.Metal3IPPool{ - Spec: ipamv1.Metal3IPPoolSpec{}, + ipPool: &ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{}, }, - ipClaim: &ipamv1.Metal3IPClaim{ + ipClaim: &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", }, @@ -719,10 +719,10 @@ var _ = Describe("Metal3IPPool manager", func() { expectError: true, }), Entry("One pool, pre-allocated", testCaseAllocateAddress{ - ipPool: &ipamv1.Metal3IPPool{ - Spec: ipamv1.Metal3IPPoolSpec{ - Pools: []ipamv1.IPPool{ - ipamv1.IPPool{ + ipPool: &ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + ipamv1.Pool{ Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.11")), End: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.20")), Prefix: 26, @@ -736,7 +736,7 @@ var _ = Describe("Metal3IPPool manager", func() { Gateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.1")), }, }, - ipClaim: &ipamv1.Metal3IPClaim{ + ipClaim: &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "TestRef", }, @@ -746,10 +746,10 @@ var _ = Describe("Metal3IPPool manager", func() { expectedPrefix: 24, }), Entry("One pool, with start and existing address", testCaseAllocateAddress{ - ipPool: &ipamv1.Metal3IPPool{ - Spec: ipamv1.Metal3IPPoolSpec{ - Pools: []ipamv1.IPPool{ - ipamv1.IPPool{ + ipPool: &ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + ipamv1.Pool{ Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.11")), End: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.20")), }, @@ -758,7 +758,7 @@ var _ = Describe("Metal3IPPool manager", func() { Gateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.1")), }, }, - ipClaim: &ipamv1.Metal3IPClaim{ + ipClaim: &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "TestRef", }, @@ -772,10 +772,10 @@ var _ = Describe("Metal3IPPool manager", func() { expectedPrefix: 24, }), Entry("One pool, with subnet and override prefix", testCaseAllocateAddress{ - ipPool: &ipamv1.Metal3IPPool{ - Spec: ipamv1.Metal3IPPoolSpec{ - Pools: []ipamv1.IPPool{ - ipamv1.IPPool{ + ipPool: &ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + ipamv1.Pool{ Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.11")), End: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.20")), Prefix: 24, @@ -786,7 +786,7 @@ var _ = Describe("Metal3IPPool manager", func() { Gateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.1.1")), }, }, - ipClaim: &ipamv1.Metal3IPClaim{ + ipClaim: &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "TestRef", }, @@ -800,14 +800,14 @@ var _ = Describe("Metal3IPPool manager", func() { expectedPrefix: 24, }), Entry("two pools, with subnet and override prefix", testCaseAllocateAddress{ - ipPool: &ipamv1.Metal3IPPool{ - Spec: ipamv1.Metal3IPPoolSpec{ - Pools: []ipamv1.IPPool{ - ipamv1.IPPool{ + ipPool: &ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + ipamv1.Pool{ Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.10")), End: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.10")), }, - ipamv1.IPPool{ + ipamv1.Pool{ Subnet: (*ipamv1.IPSubnetStr)(pointer.StringPtr("192.168.1.10/24")), Prefix: 24, Gateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.1.1")), @@ -817,7 +817,7 @@ var _ = Describe("Metal3IPPool manager", func() { Gateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.2.1")), }, }, - ipClaim: &ipamv1.Metal3IPClaim{ + ipClaim: &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "TestRef", }, @@ -831,10 +831,10 @@ var _ = Describe("Metal3IPPool manager", func() { expectedPrefix: 24, }), Entry("Exhausted pools start", testCaseAllocateAddress{ - ipPool: &ipamv1.Metal3IPPool{ - Spec: ipamv1.Metal3IPPoolSpec{ - Pools: []ipamv1.IPPool{ - ipamv1.IPPool{ + ipPool: &ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + ipamv1.Pool{ Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.10")), End: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.10")), }, @@ -843,7 +843,7 @@ var _ = Describe("Metal3IPPool manager", func() { Gateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.1")), }, }, - ipClaim: &ipamv1.Metal3IPClaim{ + ipClaim: &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "TestRef", }, @@ -854,10 +854,10 @@ var _ = Describe("Metal3IPPool manager", func() { expectError: true, }), Entry("Exhausted pools subnet", testCaseAllocateAddress{ - ipPool: &ipamv1.Metal3IPPool{ - Spec: ipamv1.Metal3IPPoolSpec{ - Pools: []ipamv1.IPPool{ - ipamv1.IPPool{ + ipPool: &ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + ipamv1.Pool{ Subnet: (*ipamv1.IPSubnetStr)(pointer.StringPtr("192.168.0.0/30")), }, }, @@ -865,7 +865,7 @@ var _ = Describe("Metal3IPPool manager", func() { Gateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.1")), }, }, - ipClaim: &ipamv1.Metal3IPClaim{ + ipClaim: &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "TestRef", }, @@ -880,9 +880,9 @@ var _ = Describe("Metal3IPPool manager", func() { ) type testCaseDeleteAddresses struct { - ipPool *ipamv1.Metal3IPPool - ipClaim *ipamv1.Metal3IPClaim - m3addresses []*ipamv1.Metal3IPAddress + ipPool *ipamv1.IPPool + ipClaim *ipamv1.IPClaim + m3addresses []*ipamv1.IPAddress addresses map[ipamv1.IPAddressStr]string expectedAddresses map[ipamv1.IPAddressStr]string expectedAllocations map[string]ipamv1.IPAddressStr @@ -908,8 +908,8 @@ var _ = Describe("Metal3IPPool manager", func() { Expect(err).NotTo(HaveOccurred()) } - // get list of Metal3IPAddress objects - addressObjects := ipamv1.Metal3IPAddressList{} + // get list of IPAddress objects + addressObjects := ipamv1.IPAddressList{} opts := &client.ListOptions{} err = c.List(context.TODO(), &addressObjects, opts) Expect(err).NotTo(HaveOccurred()) @@ -922,16 +922,16 @@ var _ = Describe("Metal3IPPool manager", func() { Expect(len(tc.ipClaim.Finalizers)).To(Equal(0)) }, Entry("Empty IPPool", testCaseDeleteAddresses{ - ipPool: &ipamv1.Metal3IPPool{}, - ipClaim: &ipamv1.Metal3IPClaim{ + ipPool: &ipamv1.IPPool{}, + ipClaim: &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "TestRef", }, }, }), Entry("No Deletion needed", testCaseDeleteAddresses{ - ipPool: &ipamv1.Metal3IPPool{}, - ipClaim: &ipamv1.Metal3IPClaim{ + ipPool: &ipamv1.IPPool{}, + ipClaim: &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "TestRef", }, @@ -942,14 +942,14 @@ var _ = Describe("Metal3IPPool manager", func() { }, }), Entry("Deletion needed, not found", testCaseDeleteAddresses{ - ipPool: &ipamv1.Metal3IPPool{ - Status: ipamv1.Metal3IPPoolStatus{ + ipPool: &ipamv1.IPPool{ + Status: ipamv1.IPPoolStatus{ Allocations: map[string]ipamv1.IPAddressStr{ "TestRef": ipamv1.IPAddressStr("192.168.0.1"), }, }, }, - ipClaim: &ipamv1.Metal3IPClaim{ + ipClaim: &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "TestRef", }, @@ -961,17 +961,17 @@ var _ = Describe("Metal3IPPool manager", func() { expectedAddresses: map[ipamv1.IPAddressStr]string{}, }), Entry("Deletion needed", testCaseDeleteAddresses{ - ipPool: &ipamv1.Metal3IPPool{ - Spec: ipamv1.Metal3IPPoolSpec{ + ipPool: &ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ NamePrefix: "abc", }, - Status: ipamv1.Metal3IPPoolStatus{ + Status: ipamv1.IPPoolStatus{ Allocations: map[string]ipamv1.IPAddressStr{ "TestRef": ipamv1.IPAddressStr("192.168.0.1"), }, }, }, - ipClaim: &ipamv1.Metal3IPClaim{ + ipClaim: &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "TestRef", Finalizers: []string{ @@ -984,8 +984,8 @@ var _ = Describe("Metal3IPPool manager", func() { }, expectedAddresses: map[ipamv1.IPAddressStr]string{}, expectedAllocations: map[string]ipamv1.IPAddressStr{}, - m3addresses: []*ipamv1.Metal3IPAddress{ - &ipamv1.Metal3IPAddress{ + m3addresses: []*ipamv1.IPAddress{ + &ipamv1.IPAddress{ ObjectMeta: metav1.ObjectMeta{ Name: "abc-192-168-0-1", }, @@ -995,7 +995,7 @@ var _ = Describe("Metal3IPPool manager", func() { ) type testCaseGetIPAddress struct { - ipAddress ipamv1.IPPool + ipAddress ipamv1.Pool index int expectError bool expectedIP ipamv1.IPAddressStr @@ -1012,19 +1012,19 @@ var _ = Describe("Metal3IPPool manager", func() { } }, Entry("Empty Start and Subnet", testCaseGetIPAddress{ - ipAddress: ipamv1.IPPool{}, + ipAddress: ipamv1.Pool{}, index: 1, expectError: true, }), Entry("Start set, no end or subnet", testCaseGetIPAddress{ - ipAddress: ipamv1.IPPool{ + ipAddress: ipamv1.Pool{ Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.10")), }, index: 1, expectedIP: ipamv1.IPAddressStr("192.168.0.11"), }), Entry("Start set, end set, subnet unset", testCaseGetIPAddress{ - ipAddress: ipamv1.IPPool{ + ipAddress: ipamv1.Pool{ Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.10")), End: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.100")), }, @@ -1032,7 +1032,7 @@ var _ = Describe("Metal3IPPool manager", func() { expectedIP: ipamv1.IPAddressStr("192.168.0.11"), }), Entry("Start set, end set, subnet unset, out of bound", testCaseGetIPAddress{ - ipAddress: ipamv1.IPPool{ + ipAddress: ipamv1.Pool{ Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.10")), End: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.100")), }, @@ -1040,7 +1040,7 @@ var _ = Describe("Metal3IPPool manager", func() { expectError: true, }), Entry("Start set, end unset, subnet set", testCaseGetIPAddress{ - ipAddress: ipamv1.IPPool{ + ipAddress: ipamv1.Pool{ Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.10")), Subnet: (*ipamv1.IPSubnetStr)(pointer.StringPtr("192.168.0.0/24")), }, @@ -1048,7 +1048,7 @@ var _ = Describe("Metal3IPPool manager", func() { expectedIP: ipamv1.IPAddressStr("192.168.0.11"), }), Entry("Start set, end unset, subnet set, out of bound", testCaseGetIPAddress{ - ipAddress: ipamv1.IPPool{ + ipAddress: ipamv1.Pool{ Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.10")), Subnet: (*ipamv1.IPSubnetStr)(pointer.StringPtr("192.168.0.0/24")), }, @@ -1056,7 +1056,7 @@ var _ = Describe("Metal3IPPool manager", func() { expectError: true, }), Entry("Start set, end unset, subnet empty", testCaseGetIPAddress{ - ipAddress: ipamv1.IPPool{ + ipAddress: ipamv1.Pool{ Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.10")), Subnet: (*ipamv1.IPSubnetStr)(pointer.StringPtr("")), }, @@ -1064,21 +1064,21 @@ var _ = Describe("Metal3IPPool manager", func() { expectError: true, }), Entry("subnet empty", testCaseGetIPAddress{ - ipAddress: ipamv1.IPPool{ + ipAddress: ipamv1.Pool{ Subnet: (*ipamv1.IPSubnetStr)(pointer.StringPtr("")), }, index: 1, expectError: true, }), Entry("Start unset, end unset, subnet set", testCaseGetIPAddress{ - ipAddress: ipamv1.IPPool{ + ipAddress: ipamv1.Pool{ Subnet: (*ipamv1.IPSubnetStr)(pointer.StringPtr("192.168.0.10/24")), }, index: 1, expectedIP: ipamv1.IPAddressStr("192.168.0.12"), }), Entry("Start unset, end unset, subnet set, out of bound", testCaseGetIPAddress{ - ipAddress: ipamv1.IPPool{ + ipAddress: ipamv1.Pool{ Subnet: (*ipamv1.IPSubnetStr)(pointer.StringPtr("192.168.0.10/24")), }, index: 250, diff --git a/ipam/manager_factory.go b/ipam/manager_factory.go index 86662e14..1f87236e 100644 --- a/ipam/manager_factory.go +++ b/ipam/manager_factory.go @@ -23,7 +23,7 @@ import ( ) type ManagerFactoryInterface interface { - NewIPPoolManager(*ipamv1.Metal3IPPool, logr.Logger) ( + NewIPPoolManager(*ipamv1.IPPool, logr.Logger) ( IPPoolManagerInterface, error, ) } @@ -39,6 +39,6 @@ func NewManagerFactory(client client.Client) ManagerFactory { } // NewIPPoolManager creates a new IPPoolManager -func (f ManagerFactory) NewIPPoolManager(ipPool *ipamv1.Metal3IPPool, metadataLog logr.Logger) (IPPoolManagerInterface, error) { +func (f ManagerFactory) NewIPPoolManager(ipPool *ipamv1.IPPool, metadataLog logr.Logger) (IPPoolManagerInterface, error) { return NewIPPoolManager(f.client, ipPool, metadataLog) } diff --git a/ipam/manager_factory_test.go b/ipam/manager_factory_test.go index 0b93bf36..5e76d8bb 100644 --- a/ipam/manager_factory_test.go +++ b/ipam/manager_factory_test.go @@ -41,7 +41,7 @@ var _ = Describe("Manager factory testing", func() { }) It("returns an IPPool manager", func() { - _, err := managerFactory.NewIPPoolManager(&ipamv1.Metal3IPPool{}, clusterLog) + _, err := managerFactory.NewIPPoolManager(&ipamv1.IPPool{}, clusterLog) Expect(err).NotTo(HaveOccurred()) }) diff --git a/ipam/mocks/zz_generated.metal3ippool_manager.go b/ipam/mocks/zz_generated.ippool_manager.go similarity index 98% rename from ipam/mocks/zz_generated.metal3ippool_manager.go rename to ipam/mocks/zz_generated.ippool_manager.go index 235e5383..40215d72 100644 --- a/ipam/mocks/zz_generated.metal3ippool_manager.go +++ b/ipam/mocks/zz_generated.ippool_manager.go @@ -17,7 +17,7 @@ // // Code generated by MockGen. DO NOT EDIT. -// Source: ./ipam/metal3ippool_manager.go +// Source: ./ipam/ippool_manager.go // Package ipam_mocks is a generated GoMock package. package ipam_mocks diff --git a/ipam/mocks/zz_generated.manager_factory.go b/ipam/mocks/zz_generated.manager_factory.go index 690deca7..cac49e23 100644 --- a/ipam/mocks/zz_generated.manager_factory.go +++ b/ipam/mocks/zz_generated.manager_factory.go @@ -54,7 +54,7 @@ func (m *MockManagerFactoryInterface) EXPECT() *MockManagerFactoryInterfaceMockR } // NewIPPoolManager mocks base method -func (m *MockManagerFactoryInterface) NewIPPoolManager(arg0 *v1alpha1.Metal3IPPool, arg1 logr.Logger) (ipam.IPPoolManagerInterface, error) { +func (m *MockManagerFactoryInterface) NewIPPoolManager(arg0 *v1alpha1.IPPool, arg1 logr.Logger) (ipam.IPPoolManagerInterface, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NewIPPoolManager", arg0, arg1) ret0, _ := ret[0].(ipam.IPPoolManagerInterface) diff --git a/ipam/remote/remote.go b/ipam/remote/remote.go deleted file mode 100644 index 4c6a23c6..00000000 --- a/ipam/remote/remote.go +++ /dev/null @@ -1,46 +0,0 @@ -/* -Copyright 2018 The Kubernetes 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 remote - -import ( - "context" - - "github.com/pkg/errors" - "k8s.io/apimachinery/pkg/types" - corev1 "k8s.io/client-go/kubernetes/typed/core/v1" - "k8s.io/client-go/tools/clientcmd" - clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3" - kcfg "sigs.k8s.io/cluster-api/util/kubeconfig" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// NewClusterClient creates a new ClusterClient. -func NewClusterClient(ctx context.Context, c client.Client, cluster *clusterv1.Cluster) (corev1.CoreV1Interface, error) { - kubeconfig, err := kcfg.FromSecret(ctx, c, types.NamespacedName{ - Name: cluster.Name, - Namespace: cluster.Namespace, - }) - if err != nil { - return nil, errors.Wrapf(err, "failed to retrieve kubeconfig secret for Cluster %q in namespace %q", - cluster.Name, cluster.Namespace) - } - - restConfig, err := clientcmd.RESTConfigFromKubeConfig(kubeconfig) - if err != nil { - return nil, errors.Wrapf(err, "failed to create client configuration for Cluster %q in namespace %q", - cluster.Name, cluster.Namespace) - } - - return corev1.NewForConfig(restConfig) -} diff --git a/ipam/remote/remote_test.go b/ipam/remote/remote_test.go deleted file mode 100644 index aa46e3d4..00000000 --- a/ipam/remote/remote_test.go +++ /dev/null @@ -1,118 +0,0 @@ -/* -Copyright 2019 The Kubernetes 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 remote - -import ( - "context" - "strings" - "testing" - - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3" - "sigs.k8s.io/cluster-api/util/secret" - "sigs.k8s.io/controller-runtime/pkg/client/fake" -) - -var ( - clusterWithValidKubeConfig = &clusterv1.Cluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test1", - Namespace: "test", - }, - } - - clusterWithInvalidKubeConfig = &clusterv1.Cluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test2-invalid", - Namespace: "test", - }, - } - - clusterWithNoKubeConfig = &clusterv1.Cluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test3", - Namespace: "test", - }, - } - - validKubeConfig = ` -clusters: -- cluster: - server: https://test-cluster-api:6443 - name: test-cluster-api -contexts: -- context: - cluster: test-cluster-api - user: kubernetes-admin - name: kubernetes-admin@test-cluster-api -current-context: kubernetes-admin@test-cluster-api -kind: Config -preferences: {} -users: -- name: kubernetes-admin -` - - validSecret = &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test1-kubeconfig", - Namespace: "test", - }, - Data: map[string][]byte{ - secret.KubeconfigDataName: []byte(validKubeConfig), - }, - } - - invalidSecret = &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test2-invalid-kubeconfig", - Namespace: "test", - }, - Data: map[string][]byte{ - secret.KubeconfigDataName: []byte("Not valid!!1"), - }, - } -) - -func TestNewClusterClient(t *testing.T) { - t.Run("cluster with valid kubeconfig", func(t *testing.T) { - client := fake.NewFakeClient(validSecret) - c, err := NewClusterClient(context.TODO(), client, clusterWithValidKubeConfig) - if err != nil { - t.Fatalf("Expected no errors, got %v", err) - } - - if c == nil { - t.Fatal("Expected actual client, got nil") - } - }) - - t.Run("cluster with no kubeconfig", func(t *testing.T) { - client := fake.NewFakeClient() - _, err := NewClusterClient(context.TODO(), client, clusterWithNoKubeConfig) - if !strings.Contains(err.Error(), "not found") { - t.Fatalf("Expected not found error, got %v", err) - } - }) - - t.Run("cluster with invalid kubeconfig", func(t *testing.T) { - client := fake.NewFakeClient(invalidSecret) - _, err := NewClusterClient(context.TODO(), client, clusterWithInvalidKubeConfig) - if err == nil || apierrors.IsNotFound(err) { - t.Fatalf("Expected error other than not found, got %v", err) - } - }) - -} diff --git a/ipam/suite_test.go b/ipam/suite_test.go index 05c53151..cab05bce 100644 --- a/ipam/suite_test.go +++ b/ipam/suite_test.go @@ -33,7 +33,6 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" - clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" logf "sigs.k8s.io/controller-runtime/pkg/log" @@ -46,12 +45,6 @@ var cfg *rest.Config var k8sClient client.Client var testEnv *envtest.Environment -const ( - clusterName = "testCluster" - metal3ClusterName = "testmetal3Cluster" - namespaceName = "testNameSpace" -) - func TestManagers(t *testing.T) { RegisterFailHandler(Fail) @@ -96,12 +89,6 @@ var _ = AfterSuite(func() { Expect(err).ToNot(HaveOccurred()) }) -var bmcOwnerRef = &metav1.OwnerReference{ - APIVersion: clusterv1.GroupVersion.String(), - Kind: "Cluster", - Name: clusterName, -} - //----------------------------------- //------ Helper functions ----------- //----------------------------------- diff --git a/ipam/utils.go b/ipam/utils.go index 0cdb1b6f..b1950d40 100644 --- a/ipam/utils.go +++ b/ipam/utils.go @@ -19,7 +19,6 @@ package ipam import ( "context" - corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -28,11 +27,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -const ( - // metal3SecretType defines the type of secret created by metal3 - metal3SecretType corev1.SecretType = "infrastructure.cluster.x-k8s.io/secret" -) - // Filter filters a list for a string. func Filter(list []string, strToFilter string) (newList []string) { for _, item := range list { diff --git a/ipam/utils_test.go b/ipam/utils_test.go index 35c0c193..9e4b4013 100644 --- a/ipam/utils_test.go +++ b/ipam/utils_test.go @@ -105,8 +105,8 @@ var _ = Describe("Metal3 manager utils", func() { }) type testCaseUpdate struct { - TestObject *ipamv1.Metal3IPClaim - ExistingObject *ipamv1.Metal3IPClaim + TestObject *ipamv1.IPClaim + ExistingObject *ipamv1.IPClaim ExpectedError bool } @@ -116,7 +116,7 @@ var _ = Describe("Metal3 manager utils", func() { if tc.ExistingObject != nil { err := c.Create(context.TODO(), tc.ExistingObject) Expect(err).NotTo(HaveOccurred()) - ipPool := ipamv1.Metal3IPClaim{} + ipPool := ipamv1.IPClaim{} err = c.Get(context.TODO(), client.ObjectKey{ Name: tc.ExistingObject.Name, @@ -136,7 +136,7 @@ var _ = Describe("Metal3 manager utils", func() { Expect(err).NotTo(HaveOccurred()) Expect(obj.Spec).To(Equal(tc.TestObject.Spec)) Expect(obj.Status).To(Equal(tc.TestObject.Status)) - savedObject := ipamv1.Metal3IPClaim{} + savedObject := ipamv1.IPClaim{} err = c.Get(context.TODO(), client.ObjectKey{ Name: tc.TestObject.Name, @@ -157,15 +157,15 @@ var _ = Describe("Metal3 manager utils", func() { } }, Entry("Object does not exist", testCaseUpdate{ - TestObject: &ipamv1.Metal3IPClaim{ + TestObject: &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: "myns", }, - Spec: ipamv1.Metal3IPClaimSpec{ + Spec: ipamv1.IPClaimSpec{ Pool: corev1.ObjectReference{Name: "abc"}, }, - Status: ipamv1.Metal3IPClaimStatus{ + Status: ipamv1.IPClaimStatus{ Address: &corev1.ObjectReference{Name: "abc"}, }, }, @@ -173,27 +173,27 @@ var _ = Describe("Metal3 manager utils", func() { ExpectedError: true, }), Entry("Object exists", testCaseUpdate{ - TestObject: &ipamv1.Metal3IPClaim{ + TestObject: &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: "myns", }, - Spec: ipamv1.Metal3IPClaimSpec{ + Spec: ipamv1.IPClaimSpec{ Pool: corev1.ObjectReference{Name: "abc"}, }, - Status: ipamv1.Metal3IPClaimStatus{ + Status: ipamv1.IPClaimStatus{ Address: &corev1.ObjectReference{Name: "abc"}, }, }, - ExistingObject: &ipamv1.Metal3IPClaim{ + ExistingObject: &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: "myns", }, - Spec: ipamv1.Metal3IPClaimSpec{ + Spec: ipamv1.IPClaimSpec{ Pool: corev1.ObjectReference{Name: "abcd"}, }, - Status: ipamv1.Metal3IPClaimStatus{ + Status: ipamv1.IPClaimStatus{ Address: &corev1.ObjectReference{Name: "abcd"}, }, }, @@ -217,7 +217,7 @@ var _ = Describe("Metal3 manager utils", func() { Expect(err).NotTo(HaveOccurred()) Expect(obj.Spec).To(Equal(tc.TestObject.Spec)) Expect(obj.Status).To(Equal(tc.TestObject.Status)) - savedObject := ipamv1.Metal3IPClaim{} + savedObject := ipamv1.IPClaim{} err = c.Get(context.TODO(), client.ObjectKey{ Name: tc.TestObject.Name, @@ -234,15 +234,15 @@ var _ = Describe("Metal3 manager utils", func() { } }, Entry("Object does not exist", testCaseUpdate{ - TestObject: &ipamv1.Metal3IPClaim{ + TestObject: &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: "myns", }, - Spec: ipamv1.Metal3IPClaimSpec{ + Spec: ipamv1.IPClaimSpec{ Pool: corev1.ObjectReference{Name: "abc"}, }, - Status: ipamv1.Metal3IPClaimStatus{ + Status: ipamv1.IPClaimStatus{ Address: &corev1.ObjectReference{Name: "abc"}, }, }, @@ -250,27 +250,27 @@ var _ = Describe("Metal3 manager utils", func() { ExpectedError: false, }), Entry("Object exists", testCaseUpdate{ - TestObject: &ipamv1.Metal3IPClaim{ + TestObject: &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: "myns", }, - Spec: ipamv1.Metal3IPClaimSpec{ + Spec: ipamv1.IPClaimSpec{ Pool: corev1.ObjectReference{Name: "abc"}, }, - Status: ipamv1.Metal3IPClaimStatus{ + Status: ipamv1.IPClaimStatus{ Address: &corev1.ObjectReference{Name: "abc"}, }, }, - ExistingObject: &ipamv1.Metal3IPClaim{ + ExistingObject: &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: "myns", }, - Spec: ipamv1.Metal3IPClaimSpec{ + Spec: ipamv1.IPClaimSpec{ Pool: corev1.ObjectReference{Name: "abcd"}, }, - Status: ipamv1.Metal3IPClaimStatus{ + Status: ipamv1.IPClaimStatus{ Address: &corev1.ObjectReference{Name: "abcd"}, }, }, diff --git a/main.go b/main.go index a53c4492..03934e44 100644 --- a/main.go +++ b/main.go @@ -18,23 +18,15 @@ package main import ( "flag" - "fmt" "os" "time" - bmoapis "github.com/metal3-io/baremetal-operator/pkg/apis" - infrav1alpha2 "github.com/metal3-io/cluster-api-provider-metal3/api/v1alpha2" - infrav1alpha3 "github.com/metal3-io/cluster-api-provider-metal3/api/v1alpha3" ipamv1 "github.com/metal3-io/ipam/api/v1alpha1" - "github.com/metal3-io/cluster-api-provider-metal3/baremetal" - capm3remote "github.com/metal3-io/cluster-api-provider-metal3/baremetal/remote" - "github.com/metal3-io/cluster-api-provider-metal3/controllers" + "github.com/metal3-io/ipam/controllers" + "github.com/metal3-io/ipam/ipam" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/discovery" "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" - "k8s.io/client-go/rest" "k8s.io/klog" "k8s.io/klog/klogr" clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3" @@ -44,24 +36,20 @@ import ( ) var ( - myscheme = runtime.NewScheme() - setupLog = ctrl.Log.WithName("setup") - waitForMetal3Controller = false - metricsAddr string - enableLeaderElection bool - syncPeriod time.Duration - webhookPort int - healthAddr string - watchNamespace string + myscheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") + metricsAddr string + enableLeaderElection bool + syncPeriod time.Duration + webhookPort int + healthAddr string + watchNamespace string ) func init() { _ = scheme.AddToScheme(myscheme) - _ = infrav1.AddToScheme(myscheme) + _ = ipamv1.AddToScheme(myscheme) _ = clusterv1.AddToScheme(myscheme) - _ = bmoapis.AddToScheme(myscheme) - _ = infrav1alpha2.AddToScheme(myscheme) - _ = infrav1alpha3.AddToScheme(myscheme) // +kubebuilder:scaffold:scheme } @@ -97,14 +85,6 @@ func main() { os.Exit(1) } - if waitForMetal3Controller { - err = waitForAPIs(ctrl.GetConfigOrDie()) - if err != nil { - setupLog.Error(err, "unable to discover required APIs") - os.Exit(1) - } - } - setupChecks(mgr) setupReconcilers(mgr) setupWebhooks(mgr) @@ -117,31 +97,6 @@ func main() { } } -func waitForAPIs(cfg *rest.Config) error { - c, err := discovery.NewDiscoveryClientForConfig(cfg) - if err != nil { - return err - } - - metal3GV := schema.GroupVersion{ - Group: "metal3.io", - Version: "v1alpha1", - } - - for { - err = discovery.ServerSupportsVersion(c, metal3GV) - if err != nil { - setupLog.Info(fmt.Sprintf("Waiting for API group %v to be available: %v", metal3GV, err)) - time.Sleep(time.Second * 10) - continue - } - setupLog.Info(fmt.Sprintf("Found API group %v", metal3GV)) - break - } - - return nil -} - func setupChecks(mgr ctrl.Manager) { if err := mgr.AddReadyzCheck("ping", healthz.Ping); err != nil { setupLog.Error(err, "unable to create ready check") @@ -158,49 +113,13 @@ func setupReconcilers(mgr ctrl.Manager) { if webhookPort != 0 { return } - if err := (&controllers.Metal3MachineReconciler{ - Client: mgr.GetClient(), - ManagerFactory: baremetal.NewManagerFactory(mgr.GetClient()), - Log: ctrl.Log.WithName("controllers").WithName("Metal3Machine"), - CapiClientGetter: capm3remote.NewClusterClient, - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "Metal3MachineReconciler") - os.Exit(1) - } - - if err := (&controllers.Metal3ClusterReconciler{ - Client: mgr.GetClient(), - ManagerFactory: baremetal.NewManagerFactory(mgr.GetClient()), - Log: ctrl.Log.WithName("controllers").WithName("Metal3Cluster"), - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "Metal3ClusterReconciler") - os.Exit(1) - } - if err := (&controllers.Metal3DataTemplateReconciler{ + if err := (&controllers.IPPoolReconciler{ Client: mgr.GetClient(), - ManagerFactory: baremetal.NewManagerFactory(mgr.GetClient()), - Log: ctrl.Log.WithName("controllers").WithName("Metal3DataTemplate"), + ManagerFactory: ipam.NewManagerFactory(mgr.GetClient()), + Log: ctrl.Log.WithName("controllers").WithName("IPPool"), }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "Metal3DataTemplateReconciler") - os.Exit(1) - } - - if err := (&controllers.Metal3DataReconciler{ - Client: mgr.GetClient(), - ManagerFactory: baremetal.NewManagerFactory(mgr.GetClient()), - Log: ctrl.Log.WithName("controllers").WithName("Metal3Data"), - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "Metal3DataReconciler") - os.Exit(1) - } - - if err := (&controllers.Metal3IPPoolReconciler{ - Client: mgr.GetClient(), - ManagerFactory: baremetal.NewManagerFactory(mgr.GetClient()), - Log: ctrl.Log.WithName("controllers").WithName("Metal3IPPool"), - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "Metal3IPPoolReconciler") + setupLog.Error(err, "unable to create controller", "controller", "IPPoolReconciler") os.Exit(1) } } @@ -209,104 +128,19 @@ func setupWebhooks(mgr ctrl.Manager) { if webhookPort == 0 { return } - if err := (&infrav1alpha2.Metal3Cluster{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Metal3Cluster") - os.Exit(1) - } - if err := (&infrav1alpha3.Metal3Cluster{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Metal3Cluster") - os.Exit(1) - } - if err := (&infrav1.Metal3Cluster{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Metal3Cluster") - os.Exit(1) - } - - if err := (&infrav1alpha2.Metal3ClusterList{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Metal3ClusterList") - os.Exit(1) - } - - if err := (&infrav1alpha3.Metal3ClusterList{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Metal3ClusterList") - os.Exit(1) - } - - if err := (&infrav1alpha2.Metal3Machine{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Metal3Machine") - os.Exit(1) - } - - if err := (&infrav1alpha3.Metal3Machine{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Metal3Machine") - os.Exit(1) - } - if err := (&infrav1.Metal3Machine{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Metal3Machine") - os.Exit(1) - } - - if err := (&infrav1alpha2.Metal3MachineList{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Metal3MachineList") - os.Exit(1) - } - - if err := (&infrav1alpha3.Metal3MachineList{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Metal3MachineList") - os.Exit(1) - } - - if err := (&infrav1alpha2.Metal3MachineTemplate{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Metal3MachineTemplate") - os.Exit(1) - } - - if err := (&infrav1alpha3.Metal3MachineTemplate{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Metal3MachineTemplate") - os.Exit(1) - } - if err := (&infrav1.Metal3MachineTemplate{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Metal3MachineTemplate") - os.Exit(1) - } - - if err := (&infrav1alpha2.Metal3MachineTemplateList{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Metal3MachineTemplateList") - os.Exit(1) - } - - if err := (&infrav1alpha3.Metal3MachineTemplateList{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Metal3MachineTemplateList") - os.Exit(1) - } - - if err := (&infrav1.Metal3DataTemplate{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Metal3DataTemplate") - os.Exit(1) - } - - if err := (&infrav1.Metal3Data{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Metal3Data") - os.Exit(1) - } - - if err := (&infrav1.Metal3DataClaim{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Metal3DataClaim") - os.Exit(1) - } - if err := (&infrav1.Metal3IPPool{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Metal3IPPool") + if err := (&ipamv1.IPPool{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "IPPool") os.Exit(1) } - if err := (&infrav1.Metal3IPAddress{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Metal3IPAddress") + if err := (&ipamv1.IPAddress{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "IPAddress") os.Exit(1) } - if err := (&infrav1.Metal3IPClaim{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Metal3IPClaim") + if err := (&ipamv1.IPClaim{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "IPClaim") os.Exit(1) } } From ae99e6821b6ad7f78cd3924ed4801fe7318fe060 Mon Sep 17 00:00:00 2001 From: maelk Date: Tue, 12 May 2020 13:23:52 +0300 Subject: [PATCH 3/9] Update kustomizations to not create namespace and cleanup kustomizations --- api/v1alpha1/ipaddress_webhook.go | 4 +- api/v1alpha1/ipclaim_webhook.go | 4 +- api/v1alpha1/ippool_webhook.go | 4 +- config/certmanager/certificate.yaml | 8 ++-- .../patches/cainjection_in_ipaddresses.yaml | 2 +- .../crd/patches/cainjection_in_ipclaims.yaml | 2 +- .../crd/patches/cainjection_in_ippools.yaml | 2 +- .../crd/patches/webhook_in_ipaddresses.yaml | 2 +- config/crd/patches/webhook_in_ipclaims.yaml | 2 +- config/crd/patches/webhook_in_ippools.yaml | 2 +- config/default/kustomization.yaml | 3 -- config/default/namespace.yaml | 4 -- config/kustomization.yaml | 2 +- config/manager/manager.yaml | 40 ++----------------- config/manager/manager_image_patch.yaml | 2 +- config/webhook/kustomization.yaml | 12 +++--- config/webhook/manager_webhook_patch.yaml | 4 +- config/webhook/manifests.yaml | 24 +++++------ config/webhook/service.yaml | 4 +- config/webhook/webhookcainjection_patch.yaml | 6 +-- .../manager_tolerations_patch.yaml | 2 +- main.go | 2 +- 22 files changed, 48 insertions(+), 89 deletions(-) delete mode 100644 config/default/namespace.yaml diff --git a/api/v1alpha1/ipaddress_webhook.go b/api/v1alpha1/ipaddress_webhook.go index b413621f..d9424870 100644 --- a/api/v1alpha1/ipaddress_webhook.go +++ b/api/v1alpha1/ipaddress_webhook.go @@ -28,8 +28,8 @@ func (c *IPAddress) SetupWebhookWithManager(mgr ctrl.Manager) error { Complete() } -// +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1alpha4-ipaddress,mutating=false,failurePolicy=fail,groups=ipam.metal3.io,resources=ipaddresses,versions=v1alpha4,name=validation.ipaddress.ipam.metal3.io,matchPolicy=Equivalent -// +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1alpha4-ipaddress,mutating=true,failurePolicy=fail,groups=ipam.metal3.io,resources=ipaddresses,versions=v1alpha4,name=default.ipaddress.ipam.metal3.io,matchPolicy=Equivalent +// +kubebuilder:webhook:verbs=create;update,path=/validate-ipam-metal3-io-v1alpha4-ipaddress,mutating=false,failurePolicy=fail,groups=ipam.metal3.io,resources=ipaddresses,versions=v1alpha4,name=validation.ipaddress.ipam.metal3.io,matchPolicy=Equivalent +// +kubebuilder:webhook:verbs=create;update,path=/mutate-ipam-metal3-io-v1alpha4-ipaddress,mutating=true,failurePolicy=fail,groups=ipam.metal3.io,resources=ipaddresses,versions=v1alpha4,name=default.ipaddress.ipam.metal3.io,matchPolicy=Equivalent var _ webhook.Defaulter = &IPAddress{} var _ webhook.Validator = &IPAddress{} diff --git a/api/v1alpha1/ipclaim_webhook.go b/api/v1alpha1/ipclaim_webhook.go index fd81eed9..75535b29 100644 --- a/api/v1alpha1/ipclaim_webhook.go +++ b/api/v1alpha1/ipclaim_webhook.go @@ -28,8 +28,8 @@ func (c *IPClaim) SetupWebhookWithManager(mgr ctrl.Manager) error { Complete() } -// +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1alpha4-ipclaim,mutating=false,failurePolicy=fail,groups=ipam.metal3.io,resources=ipclaims,versions=v1alpha4,name=validation.ipclaim.ipam.metal3.io,matchPolicy=Equivalent -// +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1alpha4-ipclaim,mutating=true,failurePolicy=fail,groups=ipam.metal3.io,resources=ipclaims,versions=v1alpha4,name=default.ipclaim.ipam.metal3.io,matchPolicy=Equivalent +// +kubebuilder:webhook:verbs=create;update,path=/validate-ipam-metal3-io-v1alpha4-ipclaim,mutating=false,failurePolicy=fail,groups=ipam.metal3.io,resources=ipclaims,versions=v1alpha4,name=validation.ipclaim.ipam.metal3.io,matchPolicy=Equivalent +// +kubebuilder:webhook:verbs=create;update,path=/mutate-ipam-metal3-io-v1alpha4-ipclaim,mutating=true,failurePolicy=fail,groups=ipam.metal3.io,resources=ipclaims,versions=v1alpha4,name=default.ipclaim.ipam.metal3.io,matchPolicy=Equivalent var _ webhook.Defaulter = &IPClaim{} var _ webhook.Validator = &IPClaim{} diff --git a/api/v1alpha1/ippool_webhook.go b/api/v1alpha1/ippool_webhook.go index 212981ee..e5eeaf0f 100644 --- a/api/v1alpha1/ippool_webhook.go +++ b/api/v1alpha1/ippool_webhook.go @@ -30,8 +30,8 @@ func (c *IPPool) SetupWebhookWithManager(mgr ctrl.Manager) error { Complete() } -// +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1alpha4-ippool,mutating=false,failurePolicy=fail,groups=ipam.metal3.io,resources=ippools,versions=v1alpha4,name=validation.ippool.ipam.metal3.io,matchPolicy=Equivalent -// +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1alpha4-ippool,mutating=true,failurePolicy=fail,groups=ipam.metal3.io,resources=ippools,versions=v1alpha4,name=default.ippool.ipam.metal3.io,matchPolicy=Equivalent +// +kubebuilder:webhook:verbs=create;update,path=/validate-ipam-metal3-io-v1alpha4-ippool,mutating=false,failurePolicy=fail,groups=ipam.metal3.io,resources=ippools,versions=v1alpha4,name=validation.ippool.ipam.metal3.io,matchPolicy=Equivalent +// +kubebuilder:webhook:verbs=create;update,path=/mutate-ipam-metal3-io-v1alpha4-ippool,mutating=true,failurePolicy=fail,groups=ipam.metal3.io,resources=ippools,versions=v1alpha4,name=default.ippool.ipam.metal3.io,matchPolicy=Equivalent var _ webhook.Defaulter = &IPPool{} var _ webhook.Validator = &IPPool{} diff --git a/config/certmanager/certificate.yaml b/config/certmanager/certificate.yaml index af4e13ff..128c4b4d 100644 --- a/config/certmanager/certificate.yaml +++ b/config/certmanager/certificate.yaml @@ -14,11 +14,11 @@ metadata: name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml namespace: system spec: - # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize + # $(IPAM_SERVICE_NAME) and $(IPAM_SERVICE_NAMESPACE) will be substituted by kustomize dnsNames: - - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc - - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local + - $(IPAM_SERVICE_NAME).$(IPAM_SERVICE_NAMESPACE).svc + - $(IPAM_SERVICE_NAME).$(IPAM_SERVICE_NAMESPACE).svc.cluster.local issuerRef: kind: Issuer name: selfsigned-issuer - secretName: $(SERVICE_NAME)-cert # this secret will not be prefixed, since it's not managed by kustomize + secretName: $(IPAM_SERVICE_NAME)-cert # this secret will not be prefixed, since it's not managed by kustomize diff --git a/config/crd/patches/cainjection_in_ipaddresses.yaml b/config/crd/patches/cainjection_in_ipaddresses.yaml index 136657db..e95d3493 100644 --- a/config/crd/patches/cainjection_in_ipaddresses.yaml +++ b/config/crd/patches/cainjection_in_ipaddresses.yaml @@ -4,5 +4,5 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: $(IPAM_CERTIFICATE_NAMESPACE)/$(IPAM_CERTIFICATE_NAME) name: ipaddresses.ipam.metal3.io diff --git a/config/crd/patches/cainjection_in_ipclaims.yaml b/config/crd/patches/cainjection_in_ipclaims.yaml index dd95cb46..e1ef9797 100644 --- a/config/crd/patches/cainjection_in_ipclaims.yaml +++ b/config/crd/patches/cainjection_in_ipclaims.yaml @@ -4,5 +4,5 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: $(IPAM_CERTIFICATE_NAMESPACE)/$(IPAM_CERTIFICATE_NAME) name: ipclaims.ipam.metal3.io diff --git a/config/crd/patches/cainjection_in_ippools.yaml b/config/crd/patches/cainjection_in_ippools.yaml index 8754c15b..7627e99d 100644 --- a/config/crd/patches/cainjection_in_ippools.yaml +++ b/config/crd/patches/cainjection_in_ippools.yaml @@ -4,5 +4,5 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: $(IPAM_CERTIFICATE_NAMESPACE)/$(IPAM_CERTIFICATE_NAME) name: ippools.ipam.metal3.io diff --git a/config/crd/patches/webhook_in_ipaddresses.yaml b/config/crd/patches/webhook_in_ipaddresses.yaml index 787de167..5b496fe3 100644 --- a/config/crd/patches/webhook_in_ipaddresses.yaml +++ b/config/crd/patches/webhook_in_ipaddresses.yaml @@ -15,5 +15,5 @@ spec: caBundle: Cg== service: namespace: system - name: webhook-service + name: ipam-webhook-service path: /convert diff --git a/config/crd/patches/webhook_in_ipclaims.yaml b/config/crd/patches/webhook_in_ipclaims.yaml index 5a3ec4e8..5953295f 100644 --- a/config/crd/patches/webhook_in_ipclaims.yaml +++ b/config/crd/patches/webhook_in_ipclaims.yaml @@ -15,5 +15,5 @@ spec: caBundle: Cg== service: namespace: system - name: webhook-service + name: ipam-webhook-service path: /convert diff --git a/config/crd/patches/webhook_in_ippools.yaml b/config/crd/patches/webhook_in_ippools.yaml index da16f11e..03f17184 100644 --- a/config/crd/patches/webhook_in_ippools.yaml +++ b/config/crd/patches/webhook_in_ippools.yaml @@ -15,5 +15,5 @@ spec: caBundle: Cg== service: namespace: system - name: webhook-service + name: ipam-webhook-service path: /convert diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index fcca2ece..1299b1a8 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -5,9 +5,6 @@ kind: Kustomization # dependency for CAPM3 namespace: capm3-system -resources: -- namespace.yaml - bases: - ../rbac - ../manager diff --git a/config/default/namespace.yaml b/config/default/namespace.yaml deleted file mode 100644 index 1ab3a725..00000000 --- a/config/default/namespace.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: system diff --git a/config/kustomization.yaml b/config/kustomization.yaml index cea7b98a..00cd240b 100644 --- a/config/kustomization.yaml +++ b/config/kustomization.yaml @@ -1,4 +1,4 @@ -namePrefix: metal3-ipam- +namePrefix: ipam- commonLabels: cluster.x-k8s.io/provider: "infrastructure-metal3" diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index fabd221f..dba1961f 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -1,18 +1,3 @@ -apiVersion: v1 -kind: Service -metadata: - name: controller-manager-service - namespace: system - labels: - control-plane: controller-manager - controller-tools.k8s.io: "1.0" -spec: - selector: - control-plane: controller-manager - controller-tools.k8s.io: "1.0" - ports: - - port: 443 ---- apiVersion: apps/v1 kind: Deployment metadata: @@ -43,13 +28,6 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace - resources: - limits: - cpu: 100m - memory: 30Mi - requests: - cpu: 100m - memory: 20Mi ports: - containerPort: 9440 name: healthz @@ -62,19 +40,7 @@ spec: httpGet: path: /healthz port: healthz - volumeMounts: - - mountPath: /tmp/cert - name: cert - readOnly: true terminationGracePeriodSeconds: 10 - volumes: - - name: cert - secret: - defaultMode: 420 - secretName: webhook-server-secret ---- -apiVersion: v1 -kind: Secret -metadata: - name: webhook-server-secret - namespace: system + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/master diff --git a/config/manager/manager_image_patch.yaml b/config/manager/manager_image_patch.yaml index 70fa68da..70c1be1c 100644 --- a/config/manager/manager_image_patch.yaml +++ b/config/manager/manager_image_patch.yaml @@ -8,5 +8,5 @@ spec: spec: containers: # Change the value of image field below to your controller image URL - - image: quay.io/metal3-io/cluster-api-provider-metal3:master + - image: quay.io/metal3-io/ipam:master name: manager diff --git a/config/webhook/kustomization.yaml b/config/webhook/kustomization.yaml index edd5cc7a..2b5e53c5 100644 --- a/config/webhook/kustomization.yaml +++ b/config/webhook/kustomization.yaml @@ -14,7 +14,7 @@ patchesStrategicMerge: - webhookcainjection_patch.yaml # Disable this value if you don't have any defaulting or validation webhook. If you don't know, you can check if the manifests.yaml file in the same directory has any contents. vars: -- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR +- name: IPAM_CERTIFICATE_NAMESPACE # namespace of the certificate CR objref: kind: Certificate group: cert-manager.io @@ -22,21 +22,21 @@ vars: name: serving-cert # this name should match the one in certificate.yaml fieldref: fieldpath: metadata.namespace -- name: CERTIFICATE_NAME +- name: IPAM_CERTIFICATE_NAME objref: kind: Certificate group: cert-manager.io version: v1alpha2 name: serving-cert # this name should match the one in certificate.yaml -- name: SERVICE_NAMESPACE # namespace of the service +- name: IPAM_SERVICE_NAMESPACE # namespace of the service objref: kind: Service version: v1 - name: webhook-service + name: ipam-webhook-service fieldref: fieldpath: metadata.namespace -- name: SERVICE_NAME +- name: IPAM_SERVICE_NAME objref: kind: Service version: v1 - name: webhook-service + name: ipam-webhook-service diff --git a/config/webhook/manager_webhook_patch.yaml b/config/webhook/manager_webhook_patch.yaml index 2a593037..4653936b 100644 --- a/config/webhook/manager_webhook_patch.yaml +++ b/config/webhook/manager_webhook_patch.yaml @@ -13,7 +13,7 @@ spec: - "--webhook-port=9443" ports: - containerPort: 9443 - name: webhook-server + name: ipam-mwh-server protocol: TCP volumeMounts: - mountPath: /tmp/k8s-webhook-server/serving-certs @@ -23,4 +23,4 @@ spec: - name: cert secret: defaultMode: 420 - secretName: $(SERVICE_NAME)-cert + secretName: $(IPAM_SERVICE_NAME)-cert diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 4dc467b7..942b8c79 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -9,9 +9,9 @@ webhooks: - clientConfig: caBundle: Cg== service: - name: webhook-service + name: ipam-webhook-service namespace: system - path: /mutate-infrastructure-cluster-x-k8s-io-v1alpha4-ipaddress + path: /mutate-ipam-metal3-io-v1alpha4-ipaddress failurePolicy: Fail matchPolicy: Equivalent name: default.ipaddress.ipam.metal3.io @@ -28,9 +28,9 @@ webhooks: - clientConfig: caBundle: Cg== service: - name: webhook-service + name: ipam-webhook-service namespace: system - path: /mutate-infrastructure-cluster-x-k8s-io-v1alpha4-ipclaim + path: /mutate-ipam-metal3-io-v1alpha4-ipclaim failurePolicy: Fail matchPolicy: Equivalent name: default.ipclaim.ipam.metal3.io @@ -47,9 +47,9 @@ webhooks: - clientConfig: caBundle: Cg== service: - name: webhook-service + name: ipam-webhook-service namespace: system - path: /mutate-infrastructure-cluster-x-k8s-io-v1alpha4-ippool + path: /mutate-ipam-metal3-io-v1alpha4-ippool failurePolicy: Fail matchPolicy: Equivalent name: default.ippool.ipam.metal3.io @@ -74,9 +74,9 @@ webhooks: - clientConfig: caBundle: Cg== service: - name: webhook-service + name: ipam-webhook-service namespace: system - path: /validate-infrastructure-cluster-x-k8s-io-v1alpha4-ipaddress + path: /validate-ipam-metal3-io-v1alpha4-ipaddress failurePolicy: Fail matchPolicy: Equivalent name: validation.ipaddress.ipam.metal3.io @@ -93,9 +93,9 @@ webhooks: - clientConfig: caBundle: Cg== service: - name: webhook-service + name: ipam-webhook-service namespace: system - path: /validate-infrastructure-cluster-x-k8s-io-v1alpha4-ipclaim + path: /validate-ipam-metal3-io-v1alpha4-ipclaim failurePolicy: Fail matchPolicy: Equivalent name: validation.ipclaim.ipam.metal3.io @@ -112,9 +112,9 @@ webhooks: - clientConfig: caBundle: Cg== service: - name: webhook-service + name: ipam-webhook-service namespace: system - path: /validate-infrastructure-cluster-x-k8s-io-v1alpha4-ippool + path: /validate-ipam-metal3-io-v1alpha4-ippool failurePolicy: Fail matchPolicy: Equivalent name: validation.ippool.ipam.metal3.io diff --git a/config/webhook/service.yaml b/config/webhook/service.yaml index 9bc95014..51172bd2 100644 --- a/config/webhook/service.yaml +++ b/config/webhook/service.yaml @@ -2,9 +2,9 @@ apiVersion: v1 kind: Service metadata: - name: webhook-service + name: ipam-webhook-service namespace: system spec: ports: - port: 443 - targetPort: webhook-server + targetPort: ipam-mwh-server diff --git a/config/webhook/webhookcainjection_patch.yaml b/config/webhook/webhookcainjection_patch.yaml index 7e79bf99..1ada284b 100644 --- a/config/webhook/webhookcainjection_patch.yaml +++ b/config/webhook/webhookcainjection_patch.yaml @@ -1,15 +1,15 @@ # This patch add annotation to admission webhook config and -# the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. +# the variables $(IPAM_CERTIFICATE_NAMESPACE) and $(IPAM_CERTIFICATE_NAME) will be substituted by kustomize. apiVersion: admissionregistration.k8s.io/v1beta1 kind: MutatingWebhookConfiguration metadata: name: mutating-webhook-configuration annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: $(IPAM_CERTIFICATE_NAMESPACE)/$(IPAM_CERTIFICATE_NAME) --- apiVersion: admissionregistration.k8s.io/v1beta1 kind: ValidatingWebhookConfiguration metadata: name: validating-webhook-configuration annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: $(IPAM_CERTIFICATE_NAMESPACE)/$(IPAM_CERTIFICATE_NAME) diff --git a/examples/provider-components/manager_tolerations_patch.yaml b/examples/provider-components/manager_tolerations_patch.yaml index 05f06e96..dd225e61 100644 --- a/examples/provider-components/manager_tolerations_patch.yaml +++ b/examples/provider-components/manager_tolerations_patch.yaml @@ -2,7 +2,7 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: metal3-ipam-controller-manager + name: ipam-controller-manager namespace: capm3-system spec: template: diff --git a/main.go b/main.go index 03934e44..e9f732aa 100644 --- a/main.go +++ b/main.go @@ -74,7 +74,7 @@ func main() { Scheme: myscheme, MetricsBindAddress: metricsAddr, LeaderElection: enableLeaderElection, - LeaderElectionID: "controller-leader-election-capm3", + LeaderElectionID: "controller-leader-election-ipam-capm3", SyncPeriod: &syncPeriod, Port: webhookPort, HealthProbeBindAddress: healthAddr, From 435b60eec582ad072f0a8b19af97e248c29f3513 Mon Sep 17 00:00:00 2001 From: maelk Date: Tue, 12 May 2020 16:20:41 +0300 Subject: [PATCH 4/9] Add CAPI in example generation for cluster object --- examples/generate.sh | 5 +++++ examples/ippool/ippool.yaml | 21 ++++++++++++++++++- .../provider-components/kustomization.yaml | 1 + 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/examples/generate.sh b/examples/generate.sh index 573796f5..864d1e91 100755 --- a/examples/generate.sh +++ b/examples/generate.sh @@ -30,6 +30,7 @@ export CLUSTER_NAME="${CLUSTER_NAME:-test1}" # Outputs. COMPONENTS_CERT_MANAGER_GENERATED_FILE=${OUTPUT_DIR}/cert-manager.yaml +COMPONENTS_CLUSTER_API_GENERATED_FILE=${SOURCE_DIR}/provider-components/core-components.yaml COMPONENTS_METAL3_GENERATED_FILE=${SOURCE_DIR}/provider-components/infrastructure-components.yaml PROVIDER_COMPONENTS_GENERATED_FILE=${OUTPUT_DIR}/provider-components.yaml @@ -79,6 +80,10 @@ echo "Generated ${IPPOOL_GENERATED_FILE}" # Get Cert-manager provider components file curl -L -o "${COMPONENTS_CERT_MANAGER_GENERATED_FILE}" https://github.com/jetstack/cert-manager/releases/download/v0.13.0/cert-manager.yaml +# Generate Cluster API provider components file. +kustomize build "github.com/kubernetes-sigs/cluster-api/config/?ref=master" > "${COMPONENTS_CLUSTER_API_GENERATED_FILE}" +echo "Generated ${COMPONENTS_CLUSTER_API_GENERATED_FILE}" + # Generate METAL3 Infrastructure Provider components file. kustomize build "${SOURCE_DIR}/../config" | envsubst > "${COMPONENTS_METAL3_GENERATED_FILE}" echo "Generated ${COMPONENTS_METAL3_GENERATED_FILE}" diff --git a/examples/ippool/ippool.yaml b/examples/ippool/ippool.yaml index 52805433..c7a7f8db 100644 --- a/examples/ippool/ippool.yaml +++ b/examples/ippool/ippool.yaml @@ -1,11 +1,30 @@ --- +apiVersion: cluster.x-k8s.io/v1alpha3 +kind: Cluster +metadata: + name: ${CLUSTER_NAME} +spec: + clusterNetwork: + services: + cidrBlocks: ["10.96.0.0/12"] + pods: + cidrBlocks: ["192.168.0.0/16"] + serviceDomain: "cluster.local" + infrastructureRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4 + kind: Metal3Cluster + name: ${CLUSTER_NAME} + controlPlaneRef: + kind: KubeadmControlPlane + apiVersion: controlplane.cluster.x-k8s.io/v1alpha3 + name: ${CLUSTER_NAME}-controlplane +--- apiVersion: ipam.metal3.io/v1alpha1 kind: IPPool metadata: name: pool1 spec: clusterName: ${CLUSTER_NAME} - --- apiVersion: ipam.metal3.io/v1alpha1 kind: IPClaim diff --git a/examples/provider-components/kustomization.yaml b/examples/provider-components/kustomization.yaml index 94ef23ef..bd3d5e72 100644 --- a/examples/provider-components/kustomization.yaml +++ b/examples/provider-components/kustomization.yaml @@ -1,6 +1,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: +- core-components.yaml - infrastructure-components.yaml patchesStrategicMerge: - manager_tolerations_patch.yaml From d58d51a691268fa1761905a5482addd1d966d20f Mon Sep 17 00:00:00 2001 From: maelk Date: Tue, 12 May 2020 17:04:24 +0300 Subject: [PATCH 5/9] Rename repo to metal3-ipam --- controllers/ippool_controller.go | 4 ++-- controllers/ippool_controller_test.go | 6 +++--- controllers/suite_test.go | 2 +- go.mod | 2 +- ipam/ippool_manager.go | 2 +- ipam/ippool_manager_test.go | 2 +- ipam/manager_factory.go | 2 +- ipam/manager_factory_test.go | 2 +- ipam/mocks/zz_generated.manager_factory.go | 4 ++-- ipam/suite_test.go | 2 +- ipam/utils_test.go | 2 +- main.go | 6 +++--- 12 files changed, 18 insertions(+), 18 deletions(-) diff --git a/controllers/ippool_controller.go b/controllers/ippool_controller.go index 914109f1..88d52802 100644 --- a/controllers/ippool_controller.go +++ b/controllers/ippool_controller.go @@ -21,8 +21,8 @@ import ( "time" "github.com/go-logr/logr" - ipamv1 "github.com/metal3-io/ipam/api/v1alpha1" - "github.com/metal3-io/ipam/ipam" + ipamv1 "github.com/metal3-io/metal3-ipam/api/v1alpha1" + "github.com/metal3-io/metal3-ipam/ipam" "github.com/pkg/errors" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" diff --git a/controllers/ippool_controller_test.go b/controllers/ippool_controller_test.go index f99eb08e..cbf370d9 100644 --- a/controllers/ippool_controller_test.go +++ b/controllers/ippool_controller_test.go @@ -24,9 +24,9 @@ import ( . "github.com/onsi/gomega" "github.com/golang/mock/gomock" - ipamv1 "github.com/metal3-io/ipam/api/v1alpha1" - "github.com/metal3-io/ipam/ipam" - ipam_mocks "github.com/metal3-io/ipam/ipam/mocks" + ipamv1 "github.com/metal3-io/metal3-ipam/api/v1alpha1" + "github.com/metal3-io/metal3-ipam/ipam" + ipam_mocks "github.com/metal3-io/metal3-ipam/ipam/mocks" "github.com/pkg/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" diff --git a/controllers/suite_test.go b/controllers/suite_test.go index e38f00a6..15bfa8dd 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -29,7 +29,7 @@ import ( "k8s.io/klog" "k8s.io/klog/klogr" - ipamv1 "github.com/metal3-io/ipam/api/v1alpha1" + ipamv1 "github.com/metal3-io/metal3-ipam/api/v1alpha1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3" "sigs.k8s.io/controller-runtime/pkg/client" diff --git a/go.mod b/go.mod index 7c939946..85786c08 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/metal3-io/ipam +module github.com/metal3-io/metal3-ipam go 1.13 diff --git a/ipam/ippool_manager.go b/ipam/ippool_manager.go index 705ced30..3e112cfa 100644 --- a/ipam/ippool_manager.go +++ b/ipam/ippool_manager.go @@ -24,7 +24,7 @@ import ( "strings" "github.com/go-logr/logr" - ipamv1 "github.com/metal3-io/ipam/api/v1alpha1" + ipamv1 "github.com/metal3-io/metal3-ipam/api/v1alpha1" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" diff --git a/ipam/ippool_manager_test.go b/ipam/ippool_manager_test.go index 7fa2e99f..4bbf4a0d 100644 --- a/ipam/ippool_manager_test.go +++ b/ipam/ippool_manager_test.go @@ -25,7 +25,7 @@ import ( . "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/gomega" - ipamv1 "github.com/metal3-io/ipam/api/v1alpha1" + ipamv1 "github.com/metal3-io/metal3-ipam/api/v1alpha1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" diff --git a/ipam/manager_factory.go b/ipam/manager_factory.go index 1f87236e..0cab4d0e 100644 --- a/ipam/manager_factory.go +++ b/ipam/manager_factory.go @@ -18,7 +18,7 @@ package ipam import ( "github.com/go-logr/logr" - ipamv1 "github.com/metal3-io/ipam/api/v1alpha1" + ipamv1 "github.com/metal3-io/metal3-ipam/api/v1alpha1" "sigs.k8s.io/controller-runtime/pkg/client" ) diff --git a/ipam/manager_factory_test.go b/ipam/manager_factory_test.go index 5e76d8bb..4f30aefc 100644 --- a/ipam/manager_factory_test.go +++ b/ipam/manager_factory_test.go @@ -20,7 +20,7 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - ipamv1 "github.com/metal3-io/ipam/api/v1alpha1" + ipamv1 "github.com/metal3-io/metal3-ipam/api/v1alpha1" "k8s.io/klog/klogr" "sigs.k8s.io/controller-runtime/pkg/client" fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" diff --git a/ipam/mocks/zz_generated.manager_factory.go b/ipam/mocks/zz_generated.manager_factory.go index cac49e23..83e53073 100644 --- a/ipam/mocks/zz_generated.manager_factory.go +++ b/ipam/mocks/zz_generated.manager_factory.go @@ -25,8 +25,8 @@ package ipam_mocks import ( logr "github.com/go-logr/logr" gomock "github.com/golang/mock/gomock" - v1alpha1 "github.com/metal3-io/ipam/api/v1alpha1" - ipam "github.com/metal3-io/ipam/ipam" + v1alpha1 "github.com/metal3-io/metal3-ipam/api/v1alpha1" + ipam "github.com/metal3-io/metal3-ipam/ipam" reflect "reflect" ) diff --git a/ipam/suite_test.go b/ipam/suite_test.go index cab05bce..f2d1553f 100644 --- a/ipam/suite_test.go +++ b/ipam/suite_test.go @@ -25,7 +25,7 @@ import ( . "github.com/onsi/gomega" _ "github.com/go-logr/logr" - ipamv1 "github.com/metal3-io/ipam/api/v1alpha1" + ipamv1 "github.com/metal3-io/metal3-ipam/api/v1alpha1" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/ipam/utils_test.go b/ipam/utils_test.go index 9e4b4013..e995a9b6 100644 --- a/ipam/utils_test.go +++ b/ipam/utils_test.go @@ -23,7 +23,7 @@ import ( . "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/gomega" - ipamv1 "github.com/metal3-io/ipam/api/v1alpha1" + ipamv1 "github.com/metal3-io/metal3-ipam/api/v1alpha1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/main.go b/main.go index e9f732aa..3fbab20a 100644 --- a/main.go +++ b/main.go @@ -21,9 +21,9 @@ import ( "os" "time" - ipamv1 "github.com/metal3-io/ipam/api/v1alpha1" - "github.com/metal3-io/ipam/controllers" - "github.com/metal3-io/ipam/ipam" + ipamv1 "github.com/metal3-io/metal3-ipam/api/v1alpha1" + "github.com/metal3-io/metal3-ipam/controllers" + "github.com/metal3-io/metal3-ipam/ipam" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" From d208d9473d7651a76f7bcd511003559c3f9be0ac Mon Sep 17 00:00:00 2001 From: maelk Date: Wed, 13 May 2020 13:48:52 +0300 Subject: [PATCH 6/9] Make clusterName optional --- api/v1alpha1/ippool_types.go | 3 +- api/v1alpha1/zz_generated.deepcopy.go | 5 ++ config/crd/bases/ipam.metal3.io_ippools.yaml | 2 - controllers/ippool_controller.go | 54 +++++++++++--------- controllers/ippool_controller_test.go | 20 ++++---- 5 files changed, 46 insertions(+), 38 deletions(-) diff --git a/api/v1alpha1/ippool_types.go b/api/v1alpha1/ippool_types.go index b27f3da4..847f7e07 100644 --- a/api/v1alpha1/ippool_types.go +++ b/api/v1alpha1/ippool_types.go @@ -54,8 +54,7 @@ type Pool struct { type IPPoolSpec struct { // ClusterName is the name of the Cluster this object belongs to. - // +kubebuilder:validation:MinLength=1 - ClusterName string `json:"clusterName"` + ClusterName *string `json:"clusterName,omitempty"` //Pools contains the list of IP addresses pools Pools []Pool `json:"pools,omitempty"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 49d17a90..cdfae2eb 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -267,6 +267,11 @@ func (in *IPPoolList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IPPoolSpec) DeepCopyInto(out *IPPoolSpec) { *out = *in + if in.ClusterName != nil { + in, out := &in.ClusterName, &out.ClusterName + *out = new(string) + **out = **in + } if in.Pools != nil { in, out := &in.Pools, &out.Pools *out = make([]Pool, len(*in)) diff --git a/config/crd/bases/ipam.metal3.io_ippools.yaml b/config/crd/bases/ipam.metal3.io_ippools.yaml index aea5483d..f8998c1f 100644 --- a/config/crd/bases/ipam.metal3.io_ippools.yaml +++ b/config/crd/bases/ipam.metal3.io_ippools.yaml @@ -49,7 +49,6 @@ spec: clusterName: description: ClusterName is the name of the Cluster this object belongs to. - minLength: 1 type: string gateway: description: Gateway is the gateway ip address @@ -105,7 +104,6 @@ spec: maximum: 128 type: integer required: - - clusterName - namePrefix type: object status: diff --git a/controllers/ippool_controller.go b/controllers/ippool_controller.go index 88d52802..e20c4535 100644 --- a/controllers/ippool_controller.go +++ b/controllers/ippool_controller.go @@ -88,24 +88,28 @@ func (r *IPPoolReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, rerr erro }() cluster := &capi.Cluster{} - key := client.ObjectKey{ - Name: ipamv1IPPool.Spec.ClusterName, - Namespace: ipamv1IPPool.Namespace, - } - - if ipamv1IPPool.ObjectMeta.Labels == nil { - ipamv1IPPool.ObjectMeta.Labels = make(map[string]string) - } - ipamv1IPPool.ObjectMeta.Labels[capi.ClusterLabelName] = ipamv1IPPool.Spec.ClusterName - ipamv1IPPool.ObjectMeta.Labels[capi.ProviderLabelName] = "infrastructure-metal3" + if ipamv1IPPool.Spec.ClusterName != nil { + key := client.ObjectKey{ + Name: *ipamv1IPPool.Spec.ClusterName, + Namespace: ipamv1IPPool.Namespace, + } - // Fetch the Cluster. Ignore an error if the deletion timestamp is set - err = r.Client.Get(ctx, key, cluster) - if ipamv1IPPool.ObjectMeta.DeletionTimestamp.IsZero() { - if err != nil { - metadataLog.Info("Error fetching cluster. It might not exist yet, Requeuing") - return ctrl.Result{}, nil + if ipamv1IPPool.ObjectMeta.Labels == nil { + ipamv1IPPool.ObjectMeta.Labels = make(map[string]string) } + ipamv1IPPool.ObjectMeta.Labels[capi.ClusterLabelName] = *ipamv1IPPool.Spec.ClusterName + ipamv1IPPool.ObjectMeta.Labels[capi.ProviderLabelName] = "infrastructure-metal3" + + // Fetch the Cluster. Ignore an error if the deletion timestamp is set + err = r.Client.Get(ctx, key, cluster) + if ipamv1IPPool.ObjectMeta.DeletionTimestamp.IsZero() { + if err != nil { + metadataLog.Info("Error fetching cluster. It might not exist yet, Requeuing") + return ctrl.Result{}, nil + } + } + } else { + cluster = nil } // Create a helper for managing the metadata object. @@ -114,15 +118,17 @@ func (r *IPPoolReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, rerr erro return ctrl.Result{}, errors.Wrapf(err, "failed to create helper for managing the IP pool") } - metadataLog = metadataLog.WithValues("cluster", cluster.Name) - if err := ipPoolMgr.SetClusterOwnerRef(cluster); err != nil { - return ctrl.Result{}, err - } + if ipamv1IPPool.Spec.ClusterName != nil && cluster != nil { + metadataLog = metadataLog.WithValues("cluster", cluster.Name) + if err := ipPoolMgr.SetClusterOwnerRef(cluster); err != nil { + return ctrl.Result{}, err + } - // Return early if the Metadata or Cluster is paused. - if util.IsPaused(cluster, ipamv1IPPool) { - metadataLog.Info("reconciliation is paused for this object") - return ctrl.Result{Requeue: true, RequeueAfter: requeueAfter}, nil + // Return early if the Metadata or Cluster is paused. + if util.IsPaused(cluster, ipamv1IPPool) { + metadataLog.Info("reconciliation is paused for this object") + return ctrl.Result{Requeue: true, RequeueAfter: requeueAfter}, nil + } } // Handle deleted metadata diff --git a/controllers/ippool_controller_test.go b/controllers/ippool_controller_test.go index cbf370d9..f02ac7ac 100644 --- a/controllers/ippool_controller_test.go +++ b/controllers/ippool_controller_test.go @@ -28,13 +28,13 @@ import ( "github.com/metal3-io/metal3-ipam/ipam" ipam_mocks "github.com/metal3-io/metal3-ipam/ipam/mocks" "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/klog/klogr" + "k8s.io/utils/pointer" capi "sigs.k8s.io/cluster-api/api/v1alpha3" - // ctrl "sigs.k8s.io/controller-runtime" - corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -139,7 +139,7 @@ var _ = Describe("IPPool controller", func() { Entry("Cluster not found", testCaseReconcile{ m3ipp: &ipamv1.IPPool{ ObjectMeta: testObjectMeta, - Spec: ipamv1.IPPoolSpec{ClusterName: "abc"}, + Spec: ipamv1.IPPoolSpec{ClusterName: pointer.StringPtr("abc")}, }, }), Entry("Deletion, Cluster not found", testCaseReconcile{ @@ -149,7 +149,7 @@ var _ = Describe("IPPool controller", func() { Namespace: "myns", DeletionTimestamp: ×tampNow, }, - Spec: ipamv1.IPPoolSpec{ClusterName: "abc"}, + Spec: ipamv1.IPPoolSpec{ClusterName: pointer.StringPtr("abc")}, }, expectManager: true, }), @@ -160,7 +160,7 @@ var _ = Describe("IPPool controller", func() { Namespace: "myns", DeletionTimestamp: ×tampNow, }, - Spec: ipamv1.IPPoolSpec{ClusterName: "abc"}, + Spec: ipamv1.IPPoolSpec{ClusterName: pointer.StringPtr("abc")}, }, expectManager: true, reconcileDeleteError: true, @@ -169,7 +169,7 @@ var _ = Describe("IPPool controller", func() { Entry("Paused cluster", testCaseReconcile{ m3ipp: &ipamv1.IPPool{ ObjectMeta: testObjectMeta, - Spec: ipamv1.IPPoolSpec{ClusterName: "abc"}, + Spec: ipamv1.IPPoolSpec{ClusterName: pointer.StringPtr("abc")}, }, cluster: &capi.Cluster{ ObjectMeta: testObjectMeta, @@ -183,7 +183,7 @@ var _ = Describe("IPPool controller", func() { Entry("Error in manager", testCaseReconcile{ m3ipp: &ipamv1.IPPool{ ObjectMeta: testObjectMeta, - Spec: ipamv1.IPPoolSpec{ClusterName: "abc"}, + Spec: ipamv1.IPPoolSpec{ClusterName: pointer.StringPtr("abc")}, }, cluster: &capi.Cluster{ ObjectMeta: testObjectMeta, @@ -193,7 +193,7 @@ var _ = Describe("IPPool controller", func() { Entry("Reconcile normal error", testCaseReconcile{ m3ipp: &ipamv1.IPPool{ ObjectMeta: testObjectMeta, - Spec: ipamv1.IPPoolSpec{ClusterName: "abc"}, + Spec: ipamv1.IPPoolSpec{ClusterName: pointer.StringPtr("abc")}, }, cluster: &capi.Cluster{ ObjectMeta: testObjectMeta, @@ -205,7 +205,7 @@ var _ = Describe("IPPool controller", func() { Entry("Reconcile normal no cluster", testCaseReconcile{ m3ipp: &ipamv1.IPPool{ ObjectMeta: testObjectMeta, - Spec: ipamv1.IPPoolSpec{ClusterName: "abc"}, + Spec: ipamv1.IPPoolSpec{ClusterName: pointer.StringPtr("abc")}, }, reconcileNormal: false, expectManager: false, @@ -213,7 +213,7 @@ var _ = Describe("IPPool controller", func() { Entry("Reconcile normal no error", testCaseReconcile{ m3ipp: &ipamv1.IPPool{ ObjectMeta: testObjectMeta, - Spec: ipamv1.IPPoolSpec{ClusterName: "abc"}, + Spec: ipamv1.IPPoolSpec{ClusterName: pointer.StringPtr("abc")}, }, cluster: &capi.Cluster{ ObjectMeta: testObjectMeta, From dd24caa181f0718b80f508e34967b2e8109dd4c7 Mon Sep 17 00:00:00 2001 From: maelk Date: Wed, 13 May 2020 14:33:15 +0300 Subject: [PATCH 7/9] Reset config folder variables --- config/certmanager/certificate.yaml | 8 ++++---- config/crd/patches/cainjection_in_ipaddresses.yaml | 2 +- config/crd/patches/cainjection_in_ipclaims.yaml | 2 +- config/crd/patches/cainjection_in_ippools.yaml | 2 +- config/crd/patches/webhook_in_ipaddresses.yaml | 2 +- config/crd/patches/webhook_in_ipclaims.yaml | 2 +- config/crd/patches/webhook_in_ippools.yaml | 2 +- config/webhook/kustomization.yaml | 12 ++++++------ config/webhook/manager_webhook_patch.yaml | 4 ++-- config/webhook/manifests.yaml | 12 ++++++------ config/webhook/service.yaml | 4 ++-- config/webhook/webhookcainjection_patch.yaml | 6 +++--- 12 files changed, 29 insertions(+), 29 deletions(-) diff --git a/config/certmanager/certificate.yaml b/config/certmanager/certificate.yaml index 128c4b4d..af4e13ff 100644 --- a/config/certmanager/certificate.yaml +++ b/config/certmanager/certificate.yaml @@ -14,11 +14,11 @@ metadata: name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml namespace: system spec: - # $(IPAM_SERVICE_NAME) and $(IPAM_SERVICE_NAMESPACE) will be substituted by kustomize + # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize dnsNames: - - $(IPAM_SERVICE_NAME).$(IPAM_SERVICE_NAMESPACE).svc - - $(IPAM_SERVICE_NAME).$(IPAM_SERVICE_NAMESPACE).svc.cluster.local + - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc + - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local issuerRef: kind: Issuer name: selfsigned-issuer - secretName: $(IPAM_SERVICE_NAME)-cert # this secret will not be prefixed, since it's not managed by kustomize + secretName: $(SERVICE_NAME)-cert # this secret will not be prefixed, since it's not managed by kustomize diff --git a/config/crd/patches/cainjection_in_ipaddresses.yaml b/config/crd/patches/cainjection_in_ipaddresses.yaml index e95d3493..136657db 100644 --- a/config/crd/patches/cainjection_in_ipaddresses.yaml +++ b/config/crd/patches/cainjection_in_ipaddresses.yaml @@ -4,5 +4,5 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - cert-manager.io/inject-ca-from: $(IPAM_CERTIFICATE_NAMESPACE)/$(IPAM_CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) name: ipaddresses.ipam.metal3.io diff --git a/config/crd/patches/cainjection_in_ipclaims.yaml b/config/crd/patches/cainjection_in_ipclaims.yaml index e1ef9797..dd95cb46 100644 --- a/config/crd/patches/cainjection_in_ipclaims.yaml +++ b/config/crd/patches/cainjection_in_ipclaims.yaml @@ -4,5 +4,5 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - cert-manager.io/inject-ca-from: $(IPAM_CERTIFICATE_NAMESPACE)/$(IPAM_CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) name: ipclaims.ipam.metal3.io diff --git a/config/crd/patches/cainjection_in_ippools.yaml b/config/crd/patches/cainjection_in_ippools.yaml index 7627e99d..8754c15b 100644 --- a/config/crd/patches/cainjection_in_ippools.yaml +++ b/config/crd/patches/cainjection_in_ippools.yaml @@ -4,5 +4,5 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - cert-manager.io/inject-ca-from: $(IPAM_CERTIFICATE_NAMESPACE)/$(IPAM_CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) name: ippools.ipam.metal3.io diff --git a/config/crd/patches/webhook_in_ipaddresses.yaml b/config/crd/patches/webhook_in_ipaddresses.yaml index 5b496fe3..787de167 100644 --- a/config/crd/patches/webhook_in_ipaddresses.yaml +++ b/config/crd/patches/webhook_in_ipaddresses.yaml @@ -15,5 +15,5 @@ spec: caBundle: Cg== service: namespace: system - name: ipam-webhook-service + name: webhook-service path: /convert diff --git a/config/crd/patches/webhook_in_ipclaims.yaml b/config/crd/patches/webhook_in_ipclaims.yaml index 5953295f..5a3ec4e8 100644 --- a/config/crd/patches/webhook_in_ipclaims.yaml +++ b/config/crd/patches/webhook_in_ipclaims.yaml @@ -15,5 +15,5 @@ spec: caBundle: Cg== service: namespace: system - name: ipam-webhook-service + name: webhook-service path: /convert diff --git a/config/crd/patches/webhook_in_ippools.yaml b/config/crd/patches/webhook_in_ippools.yaml index 03f17184..da16f11e 100644 --- a/config/crd/patches/webhook_in_ippools.yaml +++ b/config/crd/patches/webhook_in_ippools.yaml @@ -15,5 +15,5 @@ spec: caBundle: Cg== service: namespace: system - name: ipam-webhook-service + name: webhook-service path: /convert diff --git a/config/webhook/kustomization.yaml b/config/webhook/kustomization.yaml index 2b5e53c5..edd5cc7a 100644 --- a/config/webhook/kustomization.yaml +++ b/config/webhook/kustomization.yaml @@ -14,7 +14,7 @@ patchesStrategicMerge: - webhookcainjection_patch.yaml # Disable this value if you don't have any defaulting or validation webhook. If you don't know, you can check if the manifests.yaml file in the same directory has any contents. vars: -- name: IPAM_CERTIFICATE_NAMESPACE # namespace of the certificate CR +- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR objref: kind: Certificate group: cert-manager.io @@ -22,21 +22,21 @@ vars: name: serving-cert # this name should match the one in certificate.yaml fieldref: fieldpath: metadata.namespace -- name: IPAM_CERTIFICATE_NAME +- name: CERTIFICATE_NAME objref: kind: Certificate group: cert-manager.io version: v1alpha2 name: serving-cert # this name should match the one in certificate.yaml -- name: IPAM_SERVICE_NAMESPACE # namespace of the service +- name: SERVICE_NAMESPACE # namespace of the service objref: kind: Service version: v1 - name: ipam-webhook-service + name: webhook-service fieldref: fieldpath: metadata.namespace -- name: IPAM_SERVICE_NAME +- name: SERVICE_NAME objref: kind: Service version: v1 - name: ipam-webhook-service + name: webhook-service diff --git a/config/webhook/manager_webhook_patch.yaml b/config/webhook/manager_webhook_patch.yaml index 4653936b..0aafb6f5 100644 --- a/config/webhook/manager_webhook_patch.yaml +++ b/config/webhook/manager_webhook_patch.yaml @@ -13,7 +13,7 @@ spec: - "--webhook-port=9443" ports: - containerPort: 9443 - name: ipam-mwh-server + name: ipam-webhook protocol: TCP volumeMounts: - mountPath: /tmp/k8s-webhook-server/serving-certs @@ -23,4 +23,4 @@ spec: - name: cert secret: defaultMode: 420 - secretName: $(IPAM_SERVICE_NAME)-cert + secretName: $(SERVICE_NAME)-cert diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 942b8c79..d32c9b35 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -9,7 +9,7 @@ webhooks: - clientConfig: caBundle: Cg== service: - name: ipam-webhook-service + name: webhook-service namespace: system path: /mutate-ipam-metal3-io-v1alpha4-ipaddress failurePolicy: Fail @@ -28,7 +28,7 @@ webhooks: - clientConfig: caBundle: Cg== service: - name: ipam-webhook-service + name: webhook-service namespace: system path: /mutate-ipam-metal3-io-v1alpha4-ipclaim failurePolicy: Fail @@ -47,7 +47,7 @@ webhooks: - clientConfig: caBundle: Cg== service: - name: ipam-webhook-service + name: webhook-service namespace: system path: /mutate-ipam-metal3-io-v1alpha4-ippool failurePolicy: Fail @@ -74,7 +74,7 @@ webhooks: - clientConfig: caBundle: Cg== service: - name: ipam-webhook-service + name: webhook-service namespace: system path: /validate-ipam-metal3-io-v1alpha4-ipaddress failurePolicy: Fail @@ -93,7 +93,7 @@ webhooks: - clientConfig: caBundle: Cg== service: - name: ipam-webhook-service + name: webhook-service namespace: system path: /validate-ipam-metal3-io-v1alpha4-ipclaim failurePolicy: Fail @@ -112,7 +112,7 @@ webhooks: - clientConfig: caBundle: Cg== service: - name: ipam-webhook-service + name: webhook-service namespace: system path: /validate-ipam-metal3-io-v1alpha4-ippool failurePolicy: Fail diff --git a/config/webhook/service.yaml b/config/webhook/service.yaml index 51172bd2..3060bf7e 100644 --- a/config/webhook/service.yaml +++ b/config/webhook/service.yaml @@ -2,9 +2,9 @@ apiVersion: v1 kind: Service metadata: - name: ipam-webhook-service + name: webhook-service namespace: system spec: ports: - port: 443 - targetPort: ipam-mwh-server + targetPort: ipam-webhook diff --git a/config/webhook/webhookcainjection_patch.yaml b/config/webhook/webhookcainjection_patch.yaml index 1ada284b..7e79bf99 100644 --- a/config/webhook/webhookcainjection_patch.yaml +++ b/config/webhook/webhookcainjection_patch.yaml @@ -1,15 +1,15 @@ # This patch add annotation to admission webhook config and -# the variables $(IPAM_CERTIFICATE_NAMESPACE) and $(IPAM_CERTIFICATE_NAME) will be substituted by kustomize. +# the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. apiVersion: admissionregistration.k8s.io/v1beta1 kind: MutatingWebhookConfiguration metadata: name: mutating-webhook-configuration annotations: - cert-manager.io/inject-ca-from: $(IPAM_CERTIFICATE_NAMESPACE)/$(IPAM_CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) --- apiVersion: admissionregistration.k8s.io/v1beta1 kind: ValidatingWebhookConfiguration metadata: name: validating-webhook-configuration annotations: - cert-manager.io/inject-ca-from: $(IPAM_CERTIFICATE_NAMESPACE)/$(IPAM_CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) From 5aa96c6d33ffe80eb861647022fe461fef42faa8 Mon Sep 17 00:00:00 2001 From: maelk Date: Wed, 13 May 2020 14:40:00 +0300 Subject: [PATCH 8/9] Rename repository to metal3-io/ip-address-manager --- controllers/ippool_controller.go | 4 ++-- controllers/ippool_controller_test.go | 6 +++--- controllers/suite_test.go | 2 +- go.mod | 2 +- go.sum | 1 + ipam/ippool_manager.go | 2 +- ipam/ippool_manager_test.go | 2 +- ipam/manager_factory.go | 2 +- ipam/manager_factory_test.go | 2 +- ipam/mocks/zz_generated.manager_factory.go | 4 ++-- ipam/suite_test.go | 2 +- ipam/utils_test.go | 2 +- main.go | 6 +++--- 13 files changed, 19 insertions(+), 18 deletions(-) diff --git a/controllers/ippool_controller.go b/controllers/ippool_controller.go index e20c4535..2f010138 100644 --- a/controllers/ippool_controller.go +++ b/controllers/ippool_controller.go @@ -21,8 +21,8 @@ import ( "time" "github.com/go-logr/logr" - ipamv1 "github.com/metal3-io/metal3-ipam/api/v1alpha1" - "github.com/metal3-io/metal3-ipam/ipam" + ipamv1 "github.com/metal3-io/ip-address-manager/api/v1alpha1" + "github.com/metal3-io/ip-address-manager/ipam" "github.com/pkg/errors" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" diff --git a/controllers/ippool_controller_test.go b/controllers/ippool_controller_test.go index f02ac7ac..8debd8d7 100644 --- a/controllers/ippool_controller_test.go +++ b/controllers/ippool_controller_test.go @@ -24,9 +24,9 @@ import ( . "github.com/onsi/gomega" "github.com/golang/mock/gomock" - ipamv1 "github.com/metal3-io/metal3-ipam/api/v1alpha1" - "github.com/metal3-io/metal3-ipam/ipam" - ipam_mocks "github.com/metal3-io/metal3-ipam/ipam/mocks" + ipamv1 "github.com/metal3-io/ip-address-manager/api/v1alpha1" + "github.com/metal3-io/ip-address-manager/ipam" + ipam_mocks "github.com/metal3-io/ip-address-manager/ipam/mocks" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 15bfa8dd..96831e8b 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -29,7 +29,7 @@ import ( "k8s.io/klog" "k8s.io/klog/klogr" - ipamv1 "github.com/metal3-io/metal3-ipam/api/v1alpha1" + ipamv1 "github.com/metal3-io/ip-address-manager/api/v1alpha1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3" "sigs.k8s.io/controller-runtime/pkg/client" diff --git a/go.mod b/go.mod index 85786c08..db7a1144 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/metal3-io/metal3-ipam +module github.com/metal3-io/ip-address-manager go 1.13 diff --git a/go.sum b/go.sum index 7b6149c9..3387cf59 100644 --- a/go.sum +++ b/go.sum @@ -689,6 +689,7 @@ github.com/metal3-io/baremetal-operator v0.0.0-20200424085833-a1dd8aca186d h1:+z github.com/metal3-io/baremetal-operator v0.0.0-20200424085833-a1dd8aca186d/go.mod h1:hb+evBgPvwwPomUzeN5okBOf45g1ZWbQ7rfrVp7sMTY= github.com/metal3-io/cluster-api-provider-metal3 v0.3.1 h1:CRG6QHbCITRTCJAbZuRlEdc7NNzgLgobW9RcE7PJNOE= github.com/metal3-io/cluster-api-provider-metal3 v0.3.1/go.mod h1:Zh08qwz1xhpEhuSXptctZMi6gJDz2nWTYqtB2Dxo2xo= +github.com/metal3-io/ip-address-manager v0.0.0-20200512072744-57e449351c5c h1:2ejS35Lg11dhdXBGv9wWioYOa4s0DkNCHcjAjbsHJHU= github.com/mholt/certmagic v0.6.2-0.20190624175158-6a42ef9fe8c2/go.mod h1:g4cOPxcjV0oFq3qwpjSA30LReKD8AoIfwAY9VvG35NY= github.com/mibk/dupl v1.0.0 h1:aZc3jqrF9n0tUHwHt/+jsRxA8cRgA0Gdl56M7W7PoqE= github.com/mibk/dupl v1.0.0/go.mod h1:pCr4pNxxIbFGvtyCOi0c7LVjmV6duhKWV+ex5vh38ME= diff --git a/ipam/ippool_manager.go b/ipam/ippool_manager.go index 3e112cfa..a8028c67 100644 --- a/ipam/ippool_manager.go +++ b/ipam/ippool_manager.go @@ -24,7 +24,7 @@ import ( "strings" "github.com/go-logr/logr" - ipamv1 "github.com/metal3-io/metal3-ipam/api/v1alpha1" + ipamv1 "github.com/metal3-io/ip-address-manager/api/v1alpha1" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" diff --git a/ipam/ippool_manager_test.go b/ipam/ippool_manager_test.go index 4bbf4a0d..42eff670 100644 --- a/ipam/ippool_manager_test.go +++ b/ipam/ippool_manager_test.go @@ -25,7 +25,7 @@ import ( . "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/gomega" - ipamv1 "github.com/metal3-io/metal3-ipam/api/v1alpha1" + ipamv1 "github.com/metal3-io/ip-address-manager/api/v1alpha1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" diff --git a/ipam/manager_factory.go b/ipam/manager_factory.go index 0cab4d0e..02ad4ca9 100644 --- a/ipam/manager_factory.go +++ b/ipam/manager_factory.go @@ -18,7 +18,7 @@ package ipam import ( "github.com/go-logr/logr" - ipamv1 "github.com/metal3-io/metal3-ipam/api/v1alpha1" + ipamv1 "github.com/metal3-io/ip-address-manager/api/v1alpha1" "sigs.k8s.io/controller-runtime/pkg/client" ) diff --git a/ipam/manager_factory_test.go b/ipam/manager_factory_test.go index 4f30aefc..3dadc667 100644 --- a/ipam/manager_factory_test.go +++ b/ipam/manager_factory_test.go @@ -20,7 +20,7 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - ipamv1 "github.com/metal3-io/metal3-ipam/api/v1alpha1" + ipamv1 "github.com/metal3-io/ip-address-manager/api/v1alpha1" "k8s.io/klog/klogr" "sigs.k8s.io/controller-runtime/pkg/client" fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" diff --git a/ipam/mocks/zz_generated.manager_factory.go b/ipam/mocks/zz_generated.manager_factory.go index 83e53073..049d73d3 100644 --- a/ipam/mocks/zz_generated.manager_factory.go +++ b/ipam/mocks/zz_generated.manager_factory.go @@ -25,8 +25,8 @@ package ipam_mocks import ( logr "github.com/go-logr/logr" gomock "github.com/golang/mock/gomock" - v1alpha1 "github.com/metal3-io/metal3-ipam/api/v1alpha1" - ipam "github.com/metal3-io/metal3-ipam/ipam" + v1alpha1 "github.com/metal3-io/ip-address-manager/api/v1alpha1" + ipam "github.com/metal3-io/ip-address-manager/ipam" reflect "reflect" ) diff --git a/ipam/suite_test.go b/ipam/suite_test.go index f2d1553f..261319fc 100644 --- a/ipam/suite_test.go +++ b/ipam/suite_test.go @@ -25,7 +25,7 @@ import ( . "github.com/onsi/gomega" _ "github.com/go-logr/logr" - ipamv1 "github.com/metal3-io/metal3-ipam/api/v1alpha1" + ipamv1 "github.com/metal3-io/ip-address-manager/api/v1alpha1" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/ipam/utils_test.go b/ipam/utils_test.go index e995a9b6..62b4558f 100644 --- a/ipam/utils_test.go +++ b/ipam/utils_test.go @@ -23,7 +23,7 @@ import ( . "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/gomega" - ipamv1 "github.com/metal3-io/metal3-ipam/api/v1alpha1" + ipamv1 "github.com/metal3-io/ip-address-manager/api/v1alpha1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/main.go b/main.go index 3fbab20a..623734b4 100644 --- a/main.go +++ b/main.go @@ -21,9 +21,9 @@ import ( "os" "time" - ipamv1 "github.com/metal3-io/metal3-ipam/api/v1alpha1" - "github.com/metal3-io/metal3-ipam/controllers" - "github.com/metal3-io/metal3-ipam/ipam" + ipamv1 "github.com/metal3-io/ip-address-manager/api/v1alpha1" + "github.com/metal3-io/ip-address-manager/controllers" + "github.com/metal3-io/ip-address-manager/ipam" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" From 2d796ffbc2688062f47cbc51537838c93d63a819 Mon Sep 17 00:00:00 2001 From: maelk Date: Wed, 13 May 2020 15:02:21 +0300 Subject: [PATCH 9/9] Update release guidelines --- Makefile | 4 ++-- VERSIONING.md | 4 ---- docs/releasing.md | 5 +++-- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index dcad1780..fdd1fe22 100644 --- a/Makefile +++ b/Makefile @@ -51,7 +51,7 @@ RELEASE_NOTES := $(TOOLS_DIR)/$(RELEASE_NOTES_BIN) REGISTRY ?= quay.io/metal3-io STAGING_REGISTRY := quay.io/metal3-io PROD_REGISTRY := quay.io/metal3-io -IMAGE_NAME ?= ipam +IMAGE_NAME ?= ip-address-manager CONTROLLER_IMG ?= $(REGISTRY)/$(IMAGE_NAME) TAG ?= v1alpha1 ARCH ?= amd64 @@ -305,7 +305,7 @@ release: clean-release ## Builds and push container images using the latest git .PHONY: release-manifests release-manifests: $(RELEASE_DIR) ## Builds the manifests to publish with a release - kustomize build config > $(RELEASE_DIR)/infrastructure-components.yaml + kustomize build config > $(RELEASE_DIR)/ipam-components.yaml .PHONY: release-binaries release-binaries: ## Builds the binaries to publish with a release diff --git a/VERSIONING.md b/VERSIONING.md index 1faad20b..6822a8f5 100644 --- a/VERSIONING.md +++ b/VERSIONING.md @@ -9,10 +9,6 @@ as we try to follow closely the release process - We follow [Semantic Versioning (semver)](https://semver.org/). -- We try to follow Cluster API release cadence -- The cadence is subject to change if necessary, refer to the - [Milestones](https://github.com/kubernetes-sigs/cluster-api/milestones) page - for up-to-date information. - The _master_ branch is where development happens, this might include breaking changes. - The _release-X_ branches contain stable, backward compatible code. A new diff --git a/docs/releasing.md b/docs/releasing.md index 48f7cd91..2fe1c544 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -23,13 +23,14 @@ You must have docker installed. ### Expected artifacts -1. A container image of the shared cluster-api-provider-metal3 manager +1. A container image of the ip-address-manager manager 1. A git tag +1. A deployment file : ipam-components.yaml ### Artifact locations 1. The container image is found in the registry `quay.io/metal3-io` with an image - name of `ipam` and a tag that matches the release + name of `ip-address-manager` and a tag that matches the release version. The image is automatically built once the release has been created. ## Creating a release for IPAM