From a503fa86734a1314116e78cef1baa7482d4985c6 Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Fri, 28 Aug 2020 10:59:09 -0500 Subject: [PATCH] Pull controller-go back into this repository as `internal/controllerlib`. Signed-off-by: Matt Moyer --- go.mod | 1 - go.sum | 37 --- internal/controller/apicerts/certs_expirer.go | 14 +- .../controller/apicerts/certs_expirer_test.go | 8 +- internal/controller/apicerts/certs_manager.go | 14 +- .../controller/apicerts/certs_manager_test.go | 32 +-- .../controller/apicerts/certs_observer.go | 12 +- .../apicerts/certs_observer_test.go | 24 +- internal/controller/issuerconfig/publisher.go | 14 +- .../controller/issuerconfig/publisher_test.go | 40 ++-- internal/controller/utils.go | 18 +- internal/controllerlib/controller.go | 224 ++++++++++++++++++ internal/controllerlib/die.go | 15 ++ internal/controllerlib/error.go | 18 ++ internal/controllerlib/filter.go | 76 ++++++ internal/controllerlib/informer.go | 25 ++ internal/controllerlib/manager.go | 59 +++++ internal/controllerlib/option.go | 154 ++++++++++++ internal/controllerlib/option_test.go | 27 +++ internal/controllerlib/queue.go | 41 ++++ internal/controllerlib/recorder.go | 29 +++ internal/controllerlib/sync.go | 44 ++++ .../integration/examplecontroller/api/api.go | 20 ++ .../examplecontroller/controller/creating.go | 182 ++++++++++++++ .../controller/creating_test.go | 170 +++++++++++++ .../examplecontroller/controller/updating.go | 149 ++++++++++++ .../examplecontroller/starter/starter.go | 56 +++++ .../integration/examplecontroller_test.go | 161 +++++++++++++ .../test/integration/main_test.go | 32 +++ internal/controllerlib/test/library/client.go | 57 +++++ internal/controllerlib/test/library/spew.go | 23 ++ internal/controllerlib/testing.go | 29 +++ .../controllermanager/prepare_controllers.go | 14 +- .../observable_with_informer_option.go | 18 +- .../observable_with_initial_event_option.go | 10 +- 35 files changed, 1700 insertions(+), 147 deletions(-) create mode 100644 internal/controllerlib/controller.go create mode 100644 internal/controllerlib/die.go create mode 100644 internal/controllerlib/error.go create mode 100644 internal/controllerlib/filter.go create mode 100644 internal/controllerlib/informer.go create mode 100644 internal/controllerlib/manager.go create mode 100644 internal/controllerlib/option.go create mode 100644 internal/controllerlib/option_test.go create mode 100644 internal/controllerlib/queue.go create mode 100644 internal/controllerlib/recorder.go create mode 100644 internal/controllerlib/sync.go create mode 100644 internal/controllerlib/test/integration/examplecontroller/api/api.go create mode 100644 internal/controllerlib/test/integration/examplecontroller/controller/creating.go create mode 100644 internal/controllerlib/test/integration/examplecontroller/controller/creating_test.go create mode 100644 internal/controllerlib/test/integration/examplecontroller/controller/updating.go create mode 100644 internal/controllerlib/test/integration/examplecontroller/starter/starter.go create mode 100644 internal/controllerlib/test/integration/examplecontroller_test.go create mode 100644 internal/controllerlib/test/integration/main_test.go create mode 100644 internal/controllerlib/test/library/client.go create mode 100644 internal/controllerlib/test/library/spew.go create mode 100644 internal/controllerlib/testing.go diff --git a/go.mod b/go.mod index f367163f..af5064f0 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,6 @@ require ( github.com/spf13/cobra v1.0.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.6.1 - github.com/suzerain-io/controller-go v0.0.0-20200730212956-7f99b569ca9f github.com/suzerain-io/pinniped/generated/1.19/apis v0.0.0-00010101000000-000000000000 github.com/suzerain-io/pinniped/generated/1.19/client v0.0.0-00010101000000-000000000000 k8s.io/api v0.19.0 diff --git a/go.sum b/go.sum index 409e67ad..9859e7d9 100644 --- a/go.sum +++ b/go.sum @@ -120,8 +120,6 @@ github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v0.0.0-20190815234213-e83c0a1c26c8 h1:DM7gHzQfHwIj+St8zaPOI6iQEPAxOwIkskvw6s9rDaM= -github.com/evanphx/json-patch v0.0.0-20190815234213-e83c0a1c26c8/go.mod h1:pmLOTb3x90VhIKxsA9yeQG5yfOkkKnkk1h+Ql8NDYDw= github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -237,8 +235,6 @@ github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d h1:pXTK/gkVNs7Zyy github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU= github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a h1:iR3fYXUjHCR97qWS8ch1y9zPNsgXThGwjKPrYfqMPks= github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU= -github.com/golangci/golangci-lint v1.29.0 h1:0ufaO3l2R1R712cFC+KT3TtwO/IOcsloKZBavRtzrBk= -github.com/golangci/golangci-lint v1.29.0/go.mod h1:Iq2GFBB9OoolSDWD81m0iJ2MR4MwDVbi4eC93fO7wh0= github.com/golangci/golangci-lint v1.30.0 h1:UhdK5WbO0GBd7W+k2lOD7BEJH4Wsa7zKfw8m3/aEJGQ= github.com/golangci/golangci-lint v1.30.0/go.mod h1:5t0i3wHlqQc9deBBvZsP+a/4xz7cfjV+zhp5U0Mzp14= github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc h1:gLLhTLMk2/SutryVJ6D4VZCU3CUqr8YloG7FPIBWFpI= @@ -280,7 +276,6 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= -github.com/gookit/color v1.2.4/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -339,7 +334,6 @@ github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -436,16 +430,12 @@ github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:v 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.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/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.13.0 h1:M76yO2HkZASFjXL0HSoZJ1AYEmQxNJmY41Jx1zNUq1Y= github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 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.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg= -github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -502,8 +492,6 @@ github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZ github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8= github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/securego/gosec/v2 v2.3.0 h1:y/9mCF2WPDbSDpL3QDWZD3HHGrSYw0QSHnCqTfs4JPE= -github.com/securego/gosec/v2 v2.3.0/go.mod h1:UzeVyUXbxukhLeHKV3VVqo7HdoQR9MrRfFmZYotn8ME= github.com/securego/gosec/v2 v2.4.0 h1:ivAoWcY5DMs9n04Abc1VkqZBO0FL0h4ShTcVsC53lCE= github.com/securego/gosec/v2 v2.4.0/go.mod h1:0/Q4cjmlFDfDUj1+Fib61sc+U5IQb2w+Iv9/C3wPVko= github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c h1:W65qqJCIOVP4jpqPQ0YvHYKwcMEMVWIzWC5iNQQfBTU= @@ -562,12 +550,8 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/suzerain-io/controller-go v0.0.0-20200730212956-7f99b569ca9f h1:gZ6rAdl+VE9DT0yE52xY/kJZ/hOJYxwtsgGoPr5vItI= -github.com/suzerain-io/controller-go v0.0.0-20200730212956-7f99b569ca9f/go.mod h1:+v9upryFWBJac6KXKlheGHr7e3kqpk1ldH1iIMFopMs= github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2 h1:Xr9gkxfOP0KQWXKNqmwe8vEeSUiUj4Rlee9CMVX2ZUQ= github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM= -github.com/tetafro/godot v0.4.2 h1:Dib7un+rYJFUi8vN0Bk6EHheKy6fv6ZzFURHw75g6m8= -github.com/tetafro/godot v0.4.2/go.mod h1:/7NLHhv08H1+8DNj0MElpAACw1ajsCuf3TKNQxA5S+0= github.com/tetafro/godot v0.4.8 h1:h61+hQraWhdI6WYqMwAwZYCE5yxL6a9/Orw4REbabSU= github.com/tetafro/godot v0.4.8/go.mod h1:/7NLHhv08H1+8DNj0MElpAACw1ajsCuf3TKNQxA5S+0= github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e h1:RumXZ56IrCj4CL+g1b9OL/oH0QnsF976bC8xQFYUD5Q= @@ -619,7 +603,6 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -770,20 +753,14 @@ golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200117220505-0cba7a3a9ee9/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200321224714-0d839f3cf2ed/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200324003944-a576cf524670/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-20200414032229-332987a829c3/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200422022333-3d57cf2e726e/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200519015757-0d0afa43d58a/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200602230032-c00d67ef29d0/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200625211823-6506e20df31f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200701041122-1837592efa10/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200710042808-f1c4188a97a1 h1:rD1FcWVsRaMY+l8biE9jbWP5MS/CJJ/90a9TMkMgNrM= -golang.org/x/tools v0.0.0-20200710042808-f1c4188a97a1 h1:rD1FcWVsRaMY+l8biE9jbWP5MS/CJJ/90a9TMkMgNrM= -golang.org/x/tools v0.0.0-20200710042808-f1c4188a97a1/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200710042808-f1c4188a97a1/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305 h1:yaM5S0KcY0lIoZo7Fl+oi91b/DdlU2zuWpfHrpWbCS0= golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -872,21 +849,14 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.19.0-rc.0 h1:K+xi+F3RNAxpFyS1f7uHekMNprjFX7WVZDx2lJE+A3A= -k8s.io/api v0.19.0-rc.0/go.mod h1:WBGMHEmngOdQBAvJiYUgP5mGDdCWXM52yDm1gtos8C0= k8s.io/api v0.19.0 h1:XyrFIJqTYZJ2DU7FBE/bSPz7b1HvbVBuBf07oeo6eTc= k8s.io/api v0.19.0/go.mod h1:I1K45XlvTrDjmj5LoM5LuP/KYrhWbjUKT/SoPG0qTjw= -k8s.io/apimachinery v0.19.0-rc.0 h1:IBmRy0elJCGgxtCT0bHT93N+rhx+vF2DD1XXJ3ntLa8= -k8s.io/apimachinery v0.19.0-rc.0/go.mod h1:EjWiYOPi+BZennZ5pGa3JLkQ+znhEOodGy/+umjiLDU= k8s.io/apimachinery v0.19.0 h1:gjKnAda/HZp5k4xQYjL0K/Yb66IvNqjthCb03QlKpaQ= k8s.io/apimachinery v0.19.0/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA= k8s.io/apiserver v0.19.0 h1:jLhrL06wGAADbLUUQm8glSLnAGP6c7y5R3p19grkBoY= k8s.io/apiserver v0.19.0/go.mod h1:XvzqavYj73931x7FLtyagh8WibHpePJ1QwWrSJs2CLk= -k8s.io/client-go v0.19.0-rc.0 h1:6WW8MElhoLeYcLiN4ky1159XG5E39KYdmLCrV/6lNiE= -k8s.io/client-go v0.19.0-rc.0/go.mod h1:3kWGD05F7c58atlk7ep9ob1hg2Yu9NSz8gJxCNNTHhc= k8s.io/client-go v0.19.0 h1:1+0E0zfWFIWeyRhQYWzimJOyAk2UT7TiARaLNwJCf7k= k8s.io/client-go v0.19.0/go.mod h1:H9E/VT95blcFQnlyShFgnFT9ZnJOAceiUHM3MlRC+mU= -k8s.io/code-generator v0.19.0-rc.0/go.mod h1:2jgaU9hVSqti1GiO69UFSoTZcL5XAvZSrXaNnK5RVA0= k8s.io/code-generator v0.19.0/go.mod h1:moqLn7w0t9cMs4+5CQyxnfA/HV8MF6aAVENF+WZZhgk= k8s.io/component-base v0.19.0 h1:OueXf1q3RW7NlLlUCj2Dimwt7E1ys6ZqRnq53l2YuoE= k8s.io/component-base v0.19.0/go.mod h1:dKsY8BxkA+9dZIAh2aWJLL/UdASFDNtGYTCItL4LM7Y= @@ -897,12 +867,8 @@ k8s.io/klog/v2 v2.2.0 h1:XRvcwJozkgZ1UQJmfMGpvRthQHOvihEhYtDfAaxMz/A= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/kube-aggregator v0.19.0 h1:rL4fsftMaqkKjaibArYDaBeqN41CHaJzgRJjUB9IrIg= k8s.io/kube-aggregator v0.19.0/go.mod h1:1Ln45PQggFAG8xOqWPIYMxUq8WNtpPnYsbUJ39DpF/A= -k8s.io/kube-openapi v0.0.0-20200427153329-656914f816f9 h1:5NC2ITmvg8RoxoH0wgmL4zn4VZqXGsKbxrikjaQx6s4= -k8s.io/kube-openapi v0.0.0-20200427153329-656914f816f9/go.mod h1:bfCVj+qXcEaE5SCvzBaqpOySr6tuCcpPKqF6HD8nyCw= k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6 h1:+WnxoVtG8TMiudHBSEtrVL1egv36TkkJm+bA8AxicmQ= k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= -k8s.io/utils v0.0.0-20200619165400-6e3d28b6ed19 h1:7Nu2dTj82c6IaWvL7hImJzcXoTPz1MsSCH7r+0m6rfo= -k8s.io/utils v0.0.0-20200619165400-6e3d28b6ed19/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20200729134348-d5654de09c73 h1:uJmqzgNWG7XyClnU/mLPBWwfKKF1K8Hf8whTseBgJcg= k8s.io/utils v0.0.0-20200729134348-d5654de09c73/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= mvdan.cc/gofumpt v0.0.0-20200709182408-4fd085cb6d5f h1:gi7cb8HTDZ6q8VqsUpkdoFi3vxwHMneQ6+Q5Ap5hjPE= @@ -920,9 +886,6 @@ rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.9 h1:rusRLrDhjBp6aYtl9sGEvQJr6faoHoDLd0YcUBTZguI= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.9/go.mod h1:dzAXnQbTRyDlZPJX2SUPEqvnB+j7AJjtlox7PEwigU0= -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/structured-merge-diff/v4 v4.0.1 h1:YXTMot5Qz/X1iBRJhAt+vI+HVttY0WkSqqhKxQ0xVbA= sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/internal/controller/apicerts/certs_expirer.go b/internal/controller/apicerts/certs_expirer.go index c139deb5..52e85aba 100644 --- a/internal/controller/apicerts/certs_expirer.go +++ b/internal/controller/apicerts/certs_expirer.go @@ -18,9 +18,9 @@ import ( "k8s.io/client-go/kubernetes" "k8s.io/klog/v2" - "github.com/suzerain-io/controller-go" "github.com/suzerain-io/pinniped/internal/constable" pinnipedcontroller "github.com/suzerain-io/pinniped/internal/controller" + "github.com/suzerain-io/pinniped/internal/controllerlib" ) type certsExpirerController struct { @@ -33,7 +33,7 @@ type certsExpirerController struct { renewBefore time.Duration } -// NewCertsExpirerController returns a controller.Controller that will delete a +// NewCertsExpirerController returns a controllerlib.Controller that will delete a // certificate secret once it gets within some threshold of its expiration time. The // deletion forces rotation of the secret with the help of other controllers. func NewCertsExpirerController( @@ -42,9 +42,9 @@ func NewCertsExpirerController( secretInformer corev1informers.SecretInformer, withInformer pinnipedcontroller.WithInformerOptionFunc, renewBefore time.Duration, -) controller.Controller { - return controller.New( - controller.Config{ +) controllerlib.Controller { + return controllerlib.New( + controllerlib.Config{ Name: "certs-expirer-controller", Syncer: &certsExpirerController{ namespace: namespace, @@ -56,13 +56,13 @@ func NewCertsExpirerController( withInformer( secretInformer, pinnipedcontroller.NameAndNamespaceExactMatchFilterFactory(certsSecretName, namespace), - controller.InformerOption{}, + controllerlib.InformerOption{}, ), ) } // Sync implements controller.Syncer.Sync. -func (c *certsExpirerController) Sync(ctx controller.Context) error { +func (c *certsExpirerController) Sync(ctx controllerlib.Context) error { secret, err := c.secretInformer.Lister().Secrets(c.namespace).Get(certsSecretName) notFound := k8serrors.IsNotFound(err) if err != nil && !notFound { diff --git a/internal/controller/apicerts/certs_expirer_test.go b/internal/controller/apicerts/certs_expirer_test.go index 49ad3beb..e81c3729 100644 --- a/internal/controller/apicerts/certs_expirer_test.go +++ b/internal/controller/apicerts/certs_expirer_test.go @@ -24,7 +24,7 @@ import ( kubernetesfake "k8s.io/client-go/kubernetes/fake" kubetesting "k8s.io/client-go/testing" - "github.com/suzerain-io/controller-go" + "github.com/suzerain-io/pinniped/internal/controllerlib" "github.com/suzerain-io/pinniped/internal/testutil" ) @@ -247,15 +247,15 @@ func TestExpirerControllerSync(t *testing.T) { namespace, kubeAPIClient, kubeInformers.Core().V1().Secrets(), - controller.WithInformer, + controllerlib.WithInformer, test.renewBefore, ) // Must start informers before calling TestRunSynchronously(). kubeInformers.Start(ctx.Done()) - controller.TestRunSynchronously(t, c) + controllerlib.TestRunSynchronously(t, c) - err := controller.TestSync(t, c, controller.Context{ + err := controllerlib.TestSync(t, c, controllerlib.Context{ Context: ctx, }) if test.wantError != "" { diff --git a/internal/controller/apicerts/certs_manager.go b/internal/controller/apicerts/certs_manager.go index be09e0f2..db0503ce 100644 --- a/internal/controller/apicerts/certs_manager.go +++ b/internal/controller/apicerts/certs_manager.go @@ -18,9 +18,9 @@ import ( "k8s.io/klog/v2" aggregatorclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset" - "github.com/suzerain-io/controller-go" "github.com/suzerain-io/pinniped/internal/certauthority" pinnipedcontroller "github.com/suzerain-io/pinniped/internal/controller" + "github.com/suzerain-io/pinniped/internal/controllerlib" ) const ( @@ -50,9 +50,9 @@ func NewCertsManagerController( withInformer pinnipedcontroller.WithInformerOptionFunc, withInitialEvent pinnipedcontroller.WithInitialEventOptionFunc, certDuration time.Duration, -) controller.Controller { - return controller.New( - controller.Config{ +) controllerlib.Controller { + return controllerlib.New( + controllerlib.Config{ Name: "certs-manager-controller", Syncer: &certsManagerController{ namespace: namespace, @@ -65,17 +65,17 @@ func NewCertsManagerController( withInformer( secretInformer, pinnipedcontroller.NameAndNamespaceExactMatchFilterFactory(certsSecretName, namespace), - controller.InformerOption{}, + controllerlib.InformerOption{}, ), // Be sure to run once even if the Secret that the informer is watching doesn't exist. - withInitialEvent(controller.Key{ + withInitialEvent(controllerlib.Key{ Namespace: namespace, Name: certsSecretName, }), ) } -func (c *certsManagerController) Sync(ctx controller.Context) error { +func (c *certsManagerController) Sync(ctx controllerlib.Context) error { // Try to get the secret from the informer cache. _, err := c.secretInformer.Lister().Secrets(c.namespace).Get(certsSecretName) notFound := k8serrors.IsNotFound(err) diff --git a/internal/controller/apicerts/certs_manager_test.go b/internal/controller/apicerts/certs_manager_test.go index 9d089ac4..0f1d1f5f 100644 --- a/internal/controller/apicerts/certs_manager_test.go +++ b/internal/controller/apicerts/certs_manager_test.go @@ -24,8 +24,8 @@ import ( apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" aggregatorfake "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/fake" - "github.com/suzerain-io/controller-go" pinnipedv1alpha1 "github.com/suzerain-io/pinniped/generated/1.19/apis/pinniped/v1alpha1" + "github.com/suzerain-io/pinniped/internal/controllerlib" "github.com/suzerain-io/pinniped/internal/testutil" ) @@ -36,7 +36,7 @@ func TestManagerControllerOptions(t *testing.T) { var r *require.Assertions var observableWithInformerOption *testutil.ObservableWithInformerOption var observableWithInitialEventOption *testutil.ObservableWithInitialEventOption - var secretsInformerFilter controller.Filter + var secretsInformerFilter controllerlib.Filter it.Before(func() { r = require.New(t) @@ -56,7 +56,7 @@ func TestManagerControllerOptions(t *testing.T) { }) when("watching Secret objects", func() { - var subject controller.Filter + var subject controllerlib.Filter var target, wrongNamespace, wrongName, unrelated *corev1.Secret it.Before(func() { @@ -105,7 +105,7 @@ func TestManagerControllerOptions(t *testing.T) { when("starting up", func() { it("asks for an initial event because the Secret may not exist yet and it needs to run anyway", func() { - r.Equal(controller.Key{ + r.Equal(controllerlib.Key{ Namespace: installedInNamespace, Name: "api-serving-cert", }, observableWithInitialEventOption.GetInitialEventKey()) @@ -121,14 +121,14 @@ func TestManagerControllerSync(t *testing.T) { var r *require.Assertions - var subject controller.Controller + var subject controllerlib.Controller var kubeAPIClient *kubernetesfake.Clientset var aggregatorAPIClient *aggregatorfake.Clientset var kubeInformerClient *kubernetesfake.Clientset var kubeInformers kubeinformers.SharedInformerFactory var timeoutContext context.Context var timeoutContextCancel context.CancelFunc - var syncContext *controller.Context + var syncContext *controllerlib.Context // Defer starting the informers until the last possible moment so that the // nested Before's can keep adding things to the informer caches. @@ -139,16 +139,16 @@ func TestManagerControllerSync(t *testing.T) { kubeAPIClient, aggregatorAPIClient, kubeInformers.Core().V1().Secrets(), - controller.WithInformer, - controller.WithInitialEvent, + controllerlib.WithInformer, + controllerlib.WithInitialEvent, certDuration, ) // Set this at the last second to support calling subject.Name(). - syncContext = &controller.Context{ + syncContext = &controllerlib.Context{ Context: timeoutContext, Name: subject.Name(), - Key: controller.Key{ + Key: controllerlib.Key{ Namespace: installedInNamespace, Name: "api-serving-cert", }, @@ -156,7 +156,7 @@ func TestManagerControllerSync(t *testing.T) { // Must start informers before calling TestRunSynchronously() kubeInformers.Start(timeoutContext.Done()) - controller.TestRunSynchronously(t, subject) + controllerlib.TestRunSynchronously(t, subject) } it.Before(func() { @@ -203,7 +203,7 @@ func TestManagerControllerSync(t *testing.T) { it("creates the api-serving-cert Secret and updates the APIService's ca bundle", func() { startInformersAndController() - err := controller.TestSync(t, subject, *syncContext) + err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) // Check all the relevant fields from the create Secret action @@ -269,7 +269,7 @@ func TestManagerControllerSync(t *testing.T) { it("returns the update error", func() { startInformersAndController() - err := controller.TestSync(t, subject, *syncContext) + err := controllerlib.TestSync(t, subject, *syncContext) r.EqualError(err, "could not update the API service: could not update API service: update failed") }) }) @@ -287,7 +287,7 @@ func TestManagerControllerSync(t *testing.T) { it("returns an error", func() { startInformersAndController() - err := controller.TestSync(t, subject, *syncContext) + err := controllerlib.TestSync(t, subject, *syncContext) r.Error(err) r.Regexp("could not get existing version of API service: .* not found", err.Error()) }) @@ -306,7 +306,7 @@ func TestManagerControllerSync(t *testing.T) { it("returns the create error and does not update the APIService", func() { startInformersAndController() - err := controller.TestSync(t, subject, *syncContext) + err := controllerlib.TestSync(t, subject, *syncContext) r.EqualError(err, "could not create secret: create failed") r.Empty(aggregatorAPIClient.Actions()) }) @@ -327,7 +327,7 @@ func TestManagerControllerSync(t *testing.T) { it("does not need to make any API calls with its API clients", func() { startInformersAndController() - err := controller.TestSync(t, subject, *syncContext) + err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) r.Empty(kubeAPIClient.Actions()) r.Empty(aggregatorAPIClient.Actions()) diff --git a/internal/controller/apicerts/certs_observer.go b/internal/controller/apicerts/certs_observer.go index 41f98e39..a1a5108e 100644 --- a/internal/controller/apicerts/certs_observer.go +++ b/internal/controller/apicerts/certs_observer.go @@ -12,8 +12,8 @@ import ( corev1informers "k8s.io/client-go/informers/core/v1" "k8s.io/klog/v2" - "github.com/suzerain-io/controller-go" pinnipedcontroller "github.com/suzerain-io/pinniped/internal/controller" + "github.com/suzerain-io/pinniped/internal/controllerlib" "github.com/suzerain-io/pinniped/internal/provider" ) @@ -28,9 +28,9 @@ func NewCertsObserverController( dynamicCertProvider provider.DynamicTLSServingCertProvider, secretInformer corev1informers.SecretInformer, withInformer pinnipedcontroller.WithInformerOptionFunc, -) controller.Controller { - return controller.New( - controller.Config{ +) controllerlib.Controller { + return controllerlib.New( + controllerlib.Config{ Name: "certs-observer-controller", Syncer: &certsObserverController{ namespace: namespace, @@ -41,12 +41,12 @@ func NewCertsObserverController( withInformer( secretInformer, pinnipedcontroller.NameAndNamespaceExactMatchFilterFactory(certsSecretName, namespace), - controller.InformerOption{}, + controllerlib.InformerOption{}, ), ) } -func (c *certsObserverController) Sync(_ controller.Context) error { +func (c *certsObserverController) Sync(_ controllerlib.Context) error { // Try to get the secret from the informer cache. certSecret, err := c.secretInformer.Lister().Secrets(c.namespace).Get(certsSecretName) notFound := k8serrors.IsNotFound(err) diff --git a/internal/controller/apicerts/certs_observer_test.go b/internal/controller/apicerts/certs_observer_test.go index 46b0663e..9b8c5c74 100644 --- a/internal/controller/apicerts/certs_observer_test.go +++ b/internal/controller/apicerts/certs_observer_test.go @@ -18,7 +18,7 @@ import ( kubeinformers "k8s.io/client-go/informers" kubernetesfake "k8s.io/client-go/kubernetes/fake" - "github.com/suzerain-io/controller-go" + "github.com/suzerain-io/pinniped/internal/controllerlib" "github.com/suzerain-io/pinniped/internal/provider" "github.com/suzerain-io/pinniped/internal/testutil" ) @@ -29,7 +29,7 @@ func TestObserverControllerInformerFilters(t *testing.T) { var r *require.Assertions var observableWithInformerOption *testutil.ObservableWithInformerOption - var secretsInformerFilter controller.Filter + var secretsInformerFilter controllerlib.Filter it.Before(func() { r = require.New(t) @@ -45,7 +45,7 @@ func TestObserverControllerInformerFilters(t *testing.T) { }) when("watching Secret objects", func() { - var subject controller.Filter + var subject controllerlib.Filter var target, wrongNamespace, wrongName, unrelated *corev1.Secret it.Before(func() { @@ -100,12 +100,12 @@ func TestObserverControllerSync(t *testing.T) { var r *require.Assertions - var subject controller.Controller + var subject controllerlib.Controller var kubeInformerClient *kubernetesfake.Clientset var kubeInformers kubeinformers.SharedInformerFactory var timeoutContext context.Context var timeoutContextCancel context.CancelFunc - var syncContext *controller.Context + var syncContext *controllerlib.Context var dynamicCertProvider provider.DynamicTLSServingCertProvider // Defer starting the informers until the last possible moment so that the @@ -116,14 +116,14 @@ func TestObserverControllerSync(t *testing.T) { installedInNamespace, dynamicCertProvider, kubeInformers.Core().V1().Secrets(), - controller.WithInformer, + controllerlib.WithInformer, ) // Set this at the last second to support calling subject.Name(). - syncContext = &controller.Context{ + syncContext = &controllerlib.Context{ Context: timeoutContext, Name: subject.Name(), - Key: controller.Key{ + Key: controllerlib.Key{ Namespace: installedInNamespace, Name: "api-serving-cert", }, @@ -131,7 +131,7 @@ func TestObserverControllerSync(t *testing.T) { // Must start informers before calling TestRunSynchronously() kubeInformers.Start(timeoutContext.Done()) - controller.TestRunSynchronously(t, subject) + controllerlib.TestRunSynchronously(t, subject) } it.Before(func() { @@ -164,7 +164,7 @@ func TestObserverControllerSync(t *testing.T) { it("sets the dynamicCertProvider's cert and key to nil", func() { startInformersAndController() - err := controller.TestSync(t, subject, *syncContext) + err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) actualCertChain, actualKey := dynamicCertProvider.CurrentCertKeyContent() @@ -194,7 +194,7 @@ func TestObserverControllerSync(t *testing.T) { it("updates the dynamicCertProvider's cert and key", func() { startInformersAndController() - err := controller.TestSync(t, subject, *syncContext) + err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) actualCertChain, actualKey := dynamicCertProvider.CurrentCertKeyContent() @@ -220,7 +220,7 @@ func TestObserverControllerSync(t *testing.T) { it("set the missing values in the dynamicCertProvider as nil", func() { startInformersAndController() - err := controller.TestSync(t, subject, *syncContext) + err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) actualCertChain, actualKey := dynamicCertProvider.CurrentCertKeyContent() diff --git a/internal/controller/issuerconfig/publisher.go b/internal/controller/issuerconfig/publisher.go index c1ba4742..46774514 100644 --- a/internal/controller/issuerconfig/publisher.go +++ b/internal/controller/issuerconfig/publisher.go @@ -14,11 +14,11 @@ import ( "k8s.io/client-go/tools/clientcmd" "k8s.io/klog/v2" - "github.com/suzerain-io/controller-go" crdpinnipedv1alpha1 "github.com/suzerain-io/pinniped/generated/1.19/apis/crdpinniped/v1alpha1" pinnipedclientset "github.com/suzerain-io/pinniped/generated/1.19/client/clientset/versioned" crdpinnipedv1alpha1informers "github.com/suzerain-io/pinniped/generated/1.19/client/informers/externalversions/crdpinniped/v1alpha1" pinnipedcontroller "github.com/suzerain-io/pinniped/internal/controller" + "github.com/suzerain-io/pinniped/internal/controllerlib" ) const ( @@ -45,9 +45,9 @@ func NewPublisherController( configMapInformer corev1informers.ConfigMapInformer, credentialIssuerConfigInformer crdpinnipedv1alpha1informers.CredentialIssuerConfigInformer, withInformer pinnipedcontroller.WithInformerOptionFunc, -) controller.Controller { - return controller.New( - controller.Config{ +) controllerlib.Controller { + return controllerlib.New( + controllerlib.Config{ Name: "publisher-controller", Syncer: &publisherController{ namespace: namespace, @@ -60,17 +60,17 @@ func NewPublisherController( withInformer( configMapInformer, pinnipedcontroller.NameAndNamespaceExactMatchFilterFactory(clusterInfoName, ClusterInfoNamespace), - controller.InformerOption{}, + controllerlib.InformerOption{}, ), withInformer( credentialIssuerConfigInformer, pinnipedcontroller.NameAndNamespaceExactMatchFilterFactory(configName, namespace), - controller.InformerOption{}, + controllerlib.InformerOption{}, ), ) } -func (c *publisherController) Sync(ctx controller.Context) error { +func (c *publisherController) Sync(ctx controllerlib.Context) error { configMap, err := c.configMapInformer. Lister(). ConfigMaps(ClusterInfoNamespace). diff --git a/internal/controller/issuerconfig/publisher_test.go b/internal/controller/issuerconfig/publisher_test.go index 4fe5e68b..f45d8390 100644 --- a/internal/controller/issuerconfig/publisher_test.go +++ b/internal/controller/issuerconfig/publisher_test.go @@ -23,10 +23,10 @@ import ( kubernetesfake "k8s.io/client-go/kubernetes/fake" coretesting "k8s.io/client-go/testing" - "github.com/suzerain-io/controller-go" crdpinnipedv1alpha1 "github.com/suzerain-io/pinniped/generated/1.19/apis/crdpinniped/v1alpha1" pinnipedfake "github.com/suzerain-io/pinniped/generated/1.19/client/clientset/versioned/fake" pinnipedinformers "github.com/suzerain-io/pinniped/generated/1.19/client/informers/externalversions" + "github.com/suzerain-io/pinniped/internal/controllerlib" "github.com/suzerain-io/pinniped/internal/testutil" ) @@ -36,8 +36,8 @@ func TestInformerFilters(t *testing.T) { var r *require.Assertions var observableWithInformerOption *testutil.ObservableWithInformerOption - var configMapInformerFilter controller.Filter - var credentialIssuerConfigInformerFilter controller.Filter + var configMapInformerFilter controllerlib.Filter + var credentialIssuerConfigInformerFilter controllerlib.Filter it.Before(func() { r = require.New(t) @@ -57,7 +57,7 @@ func TestInformerFilters(t *testing.T) { }) when("watching ConfigMap objects", func() { - var subject controller.Filter + var subject controllerlib.Filter var target, wrongNamespace, wrongName, unrelated *corev1.ConfigMap it.Before(func() { @@ -105,7 +105,7 @@ func TestInformerFilters(t *testing.T) { }) when("watching CredentialIssuerConfig objects", func() { - var subject controller.Filter + var subject controllerlib.Filter var target, wrongNamespace, wrongName, unrelated *crdpinnipedv1alpha1.CredentialIssuerConfig it.Before(func() { @@ -168,7 +168,7 @@ func TestSync(t *testing.T) { var r *require.Assertions - var subject controller.Controller + var subject controllerlib.Controller var serverOverride *string var kubeInformerClient *kubernetesfake.Clientset var pinnipedInformerClient *pinnipedfake.Clientset @@ -177,7 +177,7 @@ func TestSync(t *testing.T) { var pinnipedAPIClient *pinnipedfake.Clientset var timeoutContext context.Context var timeoutContextCancel context.CancelFunc - var syncContext *controller.Context + var syncContext *controllerlib.Context var expectedCredentialIssuerConfig = func(expectedNamespace, expectedServerURL, expectedCAData string) (schema.GroupVersionResource, *crdpinnipedv1alpha1.CredentialIssuerConfig) { expectedCredentialIssuerConfigGVR := schema.GroupVersionResource{ @@ -211,14 +211,14 @@ func TestSync(t *testing.T) { pinnipedAPIClient, kubeInformers.Core().V1().ConfigMaps(), pinnipedInformers.Crd().V1alpha1().CredentialIssuerConfigs(), - controller.WithInformer, + controllerlib.WithInformer, ) // Set this at the last second to support calling subject.Name(). - syncContext = &controller.Context{ + syncContext = &controllerlib.Context{ Context: timeoutContext, Name: subject.Name(), - Key: controller.Key{ + Key: controllerlib.Key{ Namespace: "kube-public", Name: "cluster-info", }, @@ -227,7 +227,7 @@ func TestSync(t *testing.T) { // Must start informers before calling TestRunSynchronously() kubeInformers.Start(timeoutContext.Done()) pinnipedInformers.Start(timeoutContext.Done()) - controller.TestRunSynchronously(t, subject) + controllerlib.TestRunSynchronously(t, subject) } it.Before(func() { @@ -274,7 +274,7 @@ func TestSync(t *testing.T) { when("the CredentialIssuerConfig does not already exist", func() { it("creates a CredentialIssuerConfig", func() { startInformersAndController() - err := controller.TestSync(t, subject, *syncContext) + err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) expectedCredentialIssuerConfigGVR, expectedCredentialIssuerConfig := expectedCredentialIssuerConfig( @@ -308,7 +308,7 @@ func TestSync(t *testing.T) { it("returns the create error", func() { startInformersAndController() - err := controller.TestSync(t, subject, *syncContext) + err := controllerlib.TestSync(t, subject, *syncContext) r.EqualError(err, "could not create or update credentialissuerconfig: create failed: create failed") }) }) @@ -319,7 +319,7 @@ func TestSync(t *testing.T) { *serverOverride = "https://some-server-override" startInformersAndController() - err := controller.TestSync(t, subject, *syncContext) + err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) expectedCredentialIssuerConfigGVR, expectedCredentialIssuerConfig := expectedCredentialIssuerConfig( @@ -357,7 +357,7 @@ func TestSync(t *testing.T) { it("does not update the CredentialIssuerConfig to avoid unnecessary etcd writes/api calls", func() { startInformersAndController() - err := controller.TestSync(t, subject, *syncContext) + err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) r.Empty(pinnipedAPIClient.Actions()) @@ -378,7 +378,7 @@ func TestSync(t *testing.T) { it("updates the existing CredentialIssuerConfig", func() { startInformersAndController() - err := controller.TestSync(t, subject, *syncContext) + err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) expectedCredentialIssuerConfigGVR, expectedCredentialIssuerConfig := expectedCredentialIssuerConfig( @@ -409,7 +409,7 @@ func TestSync(t *testing.T) { it("returns the update error", func() { startInformersAndController() - err := controller.TestSync(t, subject, *syncContext) + err := controllerlib.TestSync(t, subject, *syncContext) r.EqualError(err, "could not create or update credentialissuerconfig: update failed") }) }) @@ -431,7 +431,7 @@ func TestSync(t *testing.T) { it("keeps waiting for it to exist", func() { startInformersAndController() - err := controller.TestSync(t, subject, *syncContext) + err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) r.Empty(pinnipedAPIClient.Actions()) }) @@ -451,7 +451,7 @@ func TestSync(t *testing.T) { it("keeps waiting for it to be properly formatted", func() { startInformersAndController() - err := controller.TestSync(t, subject, *syncContext) + err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) r.Empty(pinnipedAPIClient.Actions()) }) @@ -472,7 +472,7 @@ func TestSync(t *testing.T) { it("keeps waiting for one", func() { startInformersAndController() - err := controller.TestSync(t, subject, *syncContext) + err := controllerlib.TestSync(t, subject, *syncContext) r.NoError(err) r.Empty(pinnipedAPIClient.Actions()) }) diff --git a/internal/controller/utils.go b/internal/controller/utils.go index 25b7fc0e..8f0866cb 100644 --- a/internal/controller/utils.go +++ b/internal/controller/utils.go @@ -8,14 +8,14 @@ package controller import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/suzerain-io/controller-go" + "github.com/suzerain-io/pinniped/internal/controllerlib" ) -func NameAndNamespaceExactMatchFilterFactory(name, namespace string) controller.FilterFuncs { +func NameAndNamespaceExactMatchFilterFactory(name, namespace string) controllerlib.FilterFuncs { objMatchesFunc := func(obj metav1.Object) bool { return obj.GetName() == name && obj.GetNamespace() == namespace } - return controller.FilterFuncs{ + return controllerlib.FilterFuncs{ AddFunc: objMatchesFunc, UpdateFunc: func(oldObj, newObj metav1.Object) bool { return objMatchesFunc(oldObj) || objMatchesFunc(newObj) @@ -24,11 +24,11 @@ func NameAndNamespaceExactMatchFilterFactory(name, namespace string) controller. } } -// Same signature as controller.WithInformer(). +// Same signature as controllerlib.WithInformer(). type WithInformerOptionFunc func( - getter controller.InformerGetter, - filter controller.Filter, - opt controller.InformerOption) controller.Option + getter controllerlib.InformerGetter, + filter controllerlib.Filter, + opt controllerlib.InformerOption) controllerlib.Option -// Same signature as controller.WithInitialEvent(). -type WithInitialEventOptionFunc func(key controller.Key) controller.Option +// Same signature as controllerlib.WithInitialEvent(). +type WithInitialEventOptionFunc func(key controllerlib.Key) controllerlib.Option diff --git a/internal/controllerlib/controller.go b/internal/controllerlib/controller.go new file mode 100644 index 00000000..0048738d --- /dev/null +++ b/internal/controllerlib/controller.go @@ -0,0 +1,224 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package controllerlib + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/events" + "k8s.io/client-go/util/workqueue" + "k8s.io/klog/v2" +) + +// Controller interface represents a runnable Kubernetes controller. +// Cancelling the context passed will cause the controller to shutdown. +// Number of workers determine how much parallel the job processing should be. +type Controller interface { + // Run runs the controller and blocks until the controller is finished. + // Number of workers can be specified via workers parameter. + // This function will return when all internal loops are finished. + // Note that having more than one worker usually means handing parallelization of Sync(). + Run(ctx context.Context, workers int) + + // Name returns the controller name string. + Name() string + + // The methods below should only be called during tests via the Test* functions. + + // sync contains the main controller logic. + // This can be used in unit tests to exercise the Syncer by directly calling it. + sync(ctx Context) error + + // wrap wraps the main controller logic provided via the Syncer. + // This can be used in tests to synchronize asynchronous events as seen by a running controller. + // The wrapping must be done after New is called and before Run is called. + wrap(wrapper SyncWrapperFunc) + + // These are called by the Run() method but also need to be called by Test* functions sometimes. + waitForCacheSyncWithTimeout() bool + invokeAllRunOpts() +} + +var _ Controller = &controller{} + +type Config struct { + Name string + Syncer Syncer +} + +func New(config Config, opts ...Option) Controller { + c := &controller{ + config: config, + } + + // set up defaults + WithRateLimiter(workqueue.DefaultControllerRateLimiter())(c) + WithRecorder(klogRecorder{})(c) + + for _, opt := range opts { + opt(c) + } + + return c +} + +type controller struct { + config Config + + queue workqueue.RateLimitingInterface + queueWrapper Queue + maxRetries int + recorder events.EventRecorder + + run bool + runOpts []Option + + cacheSyncs []cache.InformerSynced +} + +func (c *controller) Run(ctx context.Context, workers int) { + defer utilruntime.HandleCrash(crash) // prevent panics from killing the process + + klog.InfoS("starting controller", "controller", c.Name(), "workers", workers) + + c.invokeAllRunOpts() + + if !c.waitForCacheSyncWithTimeout() { + panic(die(fmt.Sprintf("%s: timed out waiting for caches to sync", c.Name()))) + } + + var workerWg sync.WaitGroup + + // workerContext is used to track and initiate worker shutdown + workerContext, workerContextCancel := context.WithCancel(context.Background()) + + defer func() { + c.queue.ShutDown() // shutdown the controller queue first + workerContextCancel() // cancel the worker context, which tell workers to initiate shutdown + + // Wait for all workers to finish their job. + // at this point the Run() can hang and callers have to implement the logic that will kill + // this controller (SIGKILL). + workerWg.Wait() + klog.InfoS("all workers have been terminated, shutting down", "controller", c.Name(), "workers", workers) + }() + + for i := 1; i <= workers; i++ { + idx := i + klog.InfoS("starting worker", "controller", c.Name(), "worker", idx) + workerWg.Add(1) + go func() { + defer utilruntime.HandleCrash(crash) // prevent panics from killing the process + defer func() { + klog.InfoS("shutting down worker", "controller", c.Name(), "worker", idx) + workerWg.Done() + }() + c.runWorker(workerContext) + }() + } + + <-ctx.Done() // wait for controller context to be cancelled +} + +func (c *controller) invokeAllRunOpts() { + c.run = true + for _, opt := range c.runOpts { + opt(c) + } +} + +func (c *controller) Name() string { + return c.config.Name +} + +func (c *controller) sync(ctx Context) error { + return c.config.Syncer.Sync(ctx) +} + +func (c *controller) wrap(wrapper SyncWrapperFunc) { + c.runOpts = append(c.runOpts, toRunOpt(func(c *controller) { + c.config.Syncer = wrapper(c.config.Syncer) + })) +} + +func (c *controller) waitForCacheSyncWithTimeout() bool { + // prevent us from blocking forever due to a broken informer + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + return cache.WaitForCacheSync(ctx.Done(), c.cacheSyncs...) +} + +func (c *controller) add(filter Filter, object metav1.Object) { + key := filter.Parent(object) + c.queueWrapper.Add(key) +} + +// runWorker runs a single worker +// The worker is asked to terminate when the passed context is cancelled. +func (c *controller) runWorker(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + default: + c.processNextWorkItem(ctx) + } + } +} + +func (c *controller) processNextWorkItem(ctx context.Context) { + queueKey, quit := c.queue.Get() + if quit { + return + } + + key := queueKey.(Key) + defer c.queue.Done(key) + + syncCtx := Context{ + Context: ctx, + Name: c.Name(), + Key: key, + Queue: c.queueWrapper, + Recorder: c.recorder, + } + + err := c.sync(syncCtx) + c.handleKey(key, err) +} + +func (c *controller) handleKey(key Key, err error) { + if err == nil { + c.queue.Forget(key) + return + } + + retryForever := c.maxRetries <= 0 + shouldRetry := retryForever || c.queue.NumRequeues(key) < c.maxRetries + + if !shouldRetry { + utilruntime.HandleError(fmt.Errorf("%s: dropping key %v out of the queue: %w", c.Name(), key, err)) + c.queue.Forget(key) + return + } + + if errors.Is(err, ErrSyntheticRequeue) { + // logging this helps detecting wedged controllers with missing pre-requirements + klog.V(4).InfoS("requested synthetic requeue", "controller", c.Name(), "key", key) + } else { + utilruntime.HandleError(fmt.Errorf("%s: %v failed with: %w", c.Name(), key, err)) + } + + c.queue.AddRateLimited(key) +} diff --git a/internal/controllerlib/die.go b/internal/controllerlib/die.go new file mode 100644 index 00000000..5f1d717d --- /dev/null +++ b/internal/controllerlib/die.go @@ -0,0 +1,15 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package controllerlib + +type die string + +func crash(i interface{}) { + mustDie, ok := i.(die) + if ok { + panic(string(mustDie)) + } +} diff --git a/internal/controllerlib/error.go b/internal/controllerlib/error.go new file mode 100644 index 00000000..ea0f4339 --- /dev/null +++ b/internal/controllerlib/error.go @@ -0,0 +1,18 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package controllerlib + +// ErrSyntheticRequeue can be returned from a Syncer to force a retry artificially for the current key. +// This can also be done by re-adding the key to queue, but this is more convenient and has better logging. +const ErrSyntheticRequeue = constErr("synthetic requeue request") + +var _ error = constErr("") + +type constErr string + +func (e constErr) Error() string { + return string(e) +} diff --git a/internal/controllerlib/filter.go b/internal/controllerlib/filter.go new file mode 100644 index 00000000..b1dc74a0 --- /dev/null +++ b/internal/controllerlib/filter.go @@ -0,0 +1,76 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package controllerlib + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" +) + +type Filter interface { + Add(obj metav1.Object) bool + Update(oldObj, newObj metav1.Object) bool + Delete(obj metav1.Object) bool + + Parent(obj metav1.Object) Key +} + +var _ Filter = FilterFuncs{} + +type ParentFunc func(obj metav1.Object) Key + +type FilterFuncs struct { + ParentFunc ParentFunc + AddFunc func(obj metav1.Object) bool + UpdateFunc func(oldObj, newObj metav1.Object) bool + DeleteFunc func(obj metav1.Object) bool +} + +func (f FilterFuncs) Parent(obj metav1.Object) Key { + if f.ParentFunc == nil { + return Key{ + Namespace: obj.GetNamespace(), + Name: obj.GetName(), + } + } + return f.ParentFunc(obj) +} + +func (f FilterFuncs) Add(obj metav1.Object) bool { + if f.AddFunc == nil { + return false + } + return f.AddFunc(obj) +} + +func (f FilterFuncs) Update(oldObj, newObj metav1.Object) bool { + if f.UpdateFunc == nil { + return false + } + return f.UpdateFunc(oldObj, newObj) +} + +func (f FilterFuncs) Delete(obj metav1.Object) bool { + if f.DeleteFunc == nil { + return false + } + return f.DeleteFunc(obj) +} + +func FilterByNames(parentFunc ParentFunc, names ...string) Filter { + set := sets.NewString(names...) + has := func(obj metav1.Object) bool { + return set.Has(obj.GetName()) + } + return FilterFuncs{ + ParentFunc: parentFunc, + AddFunc: has, + UpdateFunc: func(oldObj, newObj metav1.Object) bool { + return has(newObj) + }, + DeleteFunc: has, + } +} diff --git a/internal/controllerlib/informer.go b/internal/controllerlib/informer.go new file mode 100644 index 00000000..42806e70 --- /dev/null +++ b/internal/controllerlib/informer.go @@ -0,0 +1,25 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package controllerlib + +import "k8s.io/client-go/tools/cache" + +type InformerGetter interface { + Informer() cache.SharedIndexInformer +} + +type InformerOption struct { + SkipSync bool + SkipEvents bool + + // TODO maybe add a field like: + // ResyncPeriod time.Duration + // to support using AddEventHandlerWithResyncPeriod + // this field would be mutually exclusive with SkipEvents + // I suspect we do not need this level of flexibility and resyncs can mask bugs in controller logic + // A related change could be an Option such as WithResyncSchedule to allow for cron style control loops + // It is unclear to me if we would ever need that since we assume that all events come from a Kube watch +} diff --git a/internal/controllerlib/manager.go b/internal/controllerlib/manager.go new file mode 100644 index 00000000..1206d3db --- /dev/null +++ b/internal/controllerlib/manager.go @@ -0,0 +1,59 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package controllerlib + +import ( + "context" + "sync" + + "k8s.io/klog/v2" +) + +type Manager interface { + Start(ctx context.Context) + WithController(controller Controller, workers int) Manager +} + +func NewManager() Manager { + return &controllerManager{} +} + +// runnableController represents single controller runnable configuration. +type runnableController struct { + controller Controller + workers int +} + +type controllerManager struct { + controllers []runnableController +} + +var _ Manager = &controllerManager{} + +func (c *controllerManager) WithController(controller Controller, workers int) Manager { + c.controllers = append(c.controllers, runnableController{ + controller: controller, + workers: workers, + }) + return c +} + +// Start will run all managed controllers and block until all controllers shutdown. +// When the context passed is cancelled, all controllers are signalled to shutdown. +func (c *controllerManager) Start(ctx context.Context) { + var wg sync.WaitGroup + wg.Add(len(c.controllers)) + for i := range c.controllers { + idx := i + go func() { + r := c.controllers[idx] + defer klog.InfoS("controller terminated", "controller", r.controller.Name()) + defer wg.Done() + r.controller.Run(ctx, r.workers) + }() + } + wg.Wait() +} diff --git a/internal/controllerlib/option.go b/internal/controllerlib/option.go new file mode 100644 index 00000000..e94486c1 --- /dev/null +++ b/internal/controllerlib/option.go @@ -0,0 +1,154 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package controllerlib + +import ( + "fmt" + "sync" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/events" + "k8s.io/client-go/util/workqueue" + "k8s.io/klog/v2" +) + +type Option func(*controller) + +func WithMaxRetries(maxRetries int) Option { + return func(c *controller) { + c.maxRetries = maxRetries + } +} + +func WithInitialEvent(key Key) Option { + return toNaiveRunOpt(func(c *controller) { + c.queueWrapper.Add(key) + }) +} + +func WithRateLimiter(limiter workqueue.RateLimiter) Option { + return func(c *controller) { + c.queue = workqueue.NewNamedRateLimitingQueue(limiter, c.Name()) + c.queueWrapper = &queueWrapper{queue: c.queue} + } +} + +func WithRecorder(recorder events.EventRecorder) Option { + return func(c *controller) { + c.recorder = recorder + } +} + +func WithInformer(getter InformerGetter, filter Filter, opt InformerOption) Option { + informer := getter.Informer() // immediately signal that we intend to use this informer in case it is lazily initialized + return toRunOpt(func(c *controller) { + if opt.SkipSync && opt.SkipEvents { + panic(die("cannot skip syncing and event handlers at the same time")) + } + + if !opt.SkipSync { + c.cacheSyncs = append(c.cacheSyncs, informer.HasSynced) + } + + if opt.SkipEvents { + return + } + + informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + object := metaOrDie(obj) + if filter.Add(object) { + klog.V(4).InfoS("handling add", + "controller", c.Name(), + "namespace", object.GetNamespace(), + "name", object.GetName(), + "selfLink", object.GetSelfLink(), // TODO: self link is deprecated so we need to extract the GVR in some other way (using a series of schemes?) + "kind", fmt.Sprintf("%T", object), + ) + c.add(filter, object) + } + }, + UpdateFunc: func(oldObj, newObj interface{}) { + oldObject := metaOrDie(oldObj) + newObject := metaOrDie(newObj) + if filter.Update(oldObject, newObject) { + klog.V(4).InfoS("handling update", + "controller", c.Name(), + "namespace", newObject.GetNamespace(), + "name", newObject.GetName(), + "selfLink", newObject.GetSelfLink(), // TODO: self link is deprecated so we need to extract the GVR in some other way (using a series of schemes?) + "kind", fmt.Sprintf("%T", newObject), + ) + c.add(filter, newObject) + } + }, + DeleteFunc: func(obj interface{}) { + accessor, err := meta.Accessor(obj) + if err != nil { + tombstone, ok := obj.(cache.DeletedFinalStateUnknown) + if !ok { + //nolint: goerr113 + utilruntime.HandleError(fmt.Errorf("%s: could not get object from tombstone: %+v", c.Name(), obj)) + return + } + accessor, err = meta.Accessor(tombstone.Obj) + if err != nil { + //nolint: goerr113 + utilruntime.HandleError(fmt.Errorf("%s: tombstone contained object that is not an accessor: %+v", c.Name(), obj)) + return + } + } + if filter.Delete(accessor) { + klog.V(4).InfoS("handling delete", + "controller", c.Name(), + "namespace", accessor.GetNamespace(), + "name", accessor.GetName(), + "selfLink", accessor.GetSelfLink(), // TODO: self link is deprecated so we need to extract the GVR in some other way (using a series of schemes?) + "kind", fmt.Sprintf("%T", accessor), + ) + c.add(filter, accessor) + } + }, + }) + }) +} + +// toRunOpt guarantees that an Option only runs once on the first call to Run (and not New), even if a controller is stopped and restarted. +func toRunOpt(opt Option) Option { + return toOnceOpt(toNaiveRunOpt(opt)) +} + +// toNaiveRunOpt guarantees that an Option only runs on calls to Run (and not New), even if a controller is stopped and restarted. +func toNaiveRunOpt(opt Option) Option { + return func(c *controller) { + if c.run { + opt(c) + return + } + c.runOpts = append(c.runOpts, opt) + } +} + +// toOnceOpt guarantees that an Option only runs once. +func toOnceOpt(opt Option) Option { + var once sync.Once + return func(c *controller) { + once.Do(func() { + opt(c) + }) + } +} + +func metaOrDie(obj interface{}) metav1.Object { + accessor, err := meta.Accessor(obj) + if err != nil { + panic(err) // this should never happen + } + return accessor +} diff --git a/internal/controllerlib/option_test.go b/internal/controllerlib/option_test.go new file mode 100644 index 00000000..47815958 --- /dev/null +++ b/internal/controllerlib/option_test.go @@ -0,0 +1,27 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package controllerlib + +import ( + "testing" + + "k8s.io/client-go/tools/cache" +) + +type getter bool + +func (g *getter) Informer() cache.SharedIndexInformer { + *g = true + return nil +} + +func TestInformerCalled(t *testing.T) { + g := getter(false) + _ = New(Config{}, WithInformer(&g, FilterByNames(nil), InformerOption{})) + if !g { + t.Error("expected InformerGetter.Informer() to be called") + } +} diff --git a/internal/controllerlib/queue.go b/internal/controllerlib/queue.go new file mode 100644 index 00000000..48756d45 --- /dev/null +++ b/internal/controllerlib/queue.go @@ -0,0 +1,41 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package controllerlib + +import ( + "time" + + "k8s.io/client-go/util/workqueue" +) + +type Queue interface { + // Add immediately adds a key to the queue and marks it as needing processing. + Add(key Key) + + // AddRateLimited adds a key to the queue after the rate limiter says it is ok. + AddRateLimited(key Key) + + // AddAfter adds a key to the queue after the indicated duration has passed. + AddAfter(key Key, duration time.Duration) +} + +var _ Queue = &queueWrapper{} + +type queueWrapper struct { + queue workqueue.RateLimitingInterface +} + +func (q *queueWrapper) Add(key Key) { + q.queue.Add(key) +} + +func (q *queueWrapper) AddRateLimited(key Key) { + q.queue.AddRateLimited(key) +} + +func (q *queueWrapper) AddAfter(key Key, duration time.Duration) { + q.queue.AddAfter(key, duration) +} diff --git a/internal/controllerlib/recorder.go b/internal/controllerlib/recorder.go new file mode 100644 index 00000000..66d91bf5 --- /dev/null +++ b/internal/controllerlib/recorder.go @@ -0,0 +1,29 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package controllerlib + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/events" + "k8s.io/klog/v2" +) + +var _ events.EventRecorder = klogRecorder{} + +type klogRecorder struct{} + +func (n klogRecorder) Eventf(regarding runtime.Object, related runtime.Object, eventtype, reason, action, note string, args ...interface{}) { + klog.V(4).InfoS("recording event", + "regarding", regarding, + "related", related, + "eventtype", eventtype, + "reason", reason, + "action", action, + "message", fmt.Sprintf(note, args...), + ) +} diff --git a/internal/controllerlib/sync.go b/internal/controllerlib/sync.go new file mode 100644 index 00000000..75266d51 --- /dev/null +++ b/internal/controllerlib/sync.go @@ -0,0 +1,44 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package controllerlib + +import ( + "context" + + "k8s.io/client-go/tools/events" +) + +var _ Syncer = SyncFunc(nil) + +type Syncer interface { + Sync(ctx Context) error +} + +type SyncFunc func(ctx Context) error + +func (s SyncFunc) Sync(ctx Context) error { + return s(ctx) +} + +type Context struct { + Context context.Context + Name string + Key Key + Queue Queue + Recorder events.EventRecorder +} + +type Key struct { + Namespace string + Name string + + // TODO determine if it makes sense to add a field like: + // Extra interface{} + // This would allow a custom ParentFunc to pass extra data through to the Syncer + // The boxed type would have to be comparable (i.e. usable as a map key) +} + +type SyncWrapperFunc func(syncer Syncer) Syncer diff --git a/internal/controllerlib/test/integration/examplecontroller/api/api.go b/internal/controllerlib/test/integration/examplecontroller/api/api.go new file mode 100644 index 00000000..1f3626bd --- /dev/null +++ b/internal/controllerlib/test/integration/examplecontroller/api/api.go @@ -0,0 +1,20 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package api + +// Annotation on service. +const SecretNameAnnotation = "service.placeholder.io/secret-name" + +// Annotations on secret. +const ( + // ServiceUIDAnnotation is an annotation on a secret that indicates which service created it, by UID + ServiceUIDAnnotation = "service.placeholder.io/service-uid" + // ServiceNameAnnotation is an annotation on a secret that indicates which service created it, by Name + // to allow reverse lookups on services for comparison against UIDs + ServiceNameAnnotation = "service.placeholder.io/service-name" +) + +const SecretDataKey = "secret-data" diff --git a/internal/controllerlib/test/integration/examplecontroller/controller/creating.go b/internal/controllerlib/test/integration/examplecontroller/controller/creating.go new file mode 100644 index 00000000..6c0c6e1d --- /dev/null +++ b/internal/controllerlib/test/integration/examplecontroller/controller/creating.go @@ -0,0 +1,182 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package controller + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + corev1informers "k8s.io/client-go/informers/core/v1" + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/tools/events" + "k8s.io/klog/v2" + + "github.com/suzerain-io/pinniped/internal/controllerlib" + "github.com/suzerain-io/pinniped/internal/controllerlib/test/integration/examplecontroller/api" +) + +//nolint:funlen +func NewExampleCreatingController( + services corev1informers.ServiceInformer, + secrets corev1informers.SecretInformer, + secretClient corev1client.SecretsGetter, + recorder events.EventRecorder, + secretData string, +) controllerlib.Controller { + serviceLister := services.Lister() + secretLister := secrets.Lister() + + // note that these functions do not need to be inlined + // this just demonstrates that for simple Syncer implementations, everything can be in one place + + requiresSecretGeneration := func(service *corev1.Service) (bool, error) { + // check the secret since it could not have been created yet + secretName := service.Annotations[api.SecretNameAnnotation] + if len(secretName) == 0 { + return false, nil + } + + secret, err := secretLister.Secrets(service.Namespace).Get(secretName) + if apierrors.IsNotFound(err) { + // we have not created the secret yet + return true, nil + } + if err != nil { + return false, fmt.Errorf("unable to get the secret %s/%s: %w", service.Namespace, secretName, err) + } + + if string(secret.Data[api.SecretDataKey]) == secretData { + return false, nil + } + + // the secret exists but the data does not match what we expect (i.e. we have new secretData now) + return true, nil + } + + generateSecret := func(service *corev1.Service) error { + klog.V(4).InfoS("generating new secret for service", "namespace", service.Namespace, "name", service.Name) + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: service.Annotations[api.SecretNameAnnotation], + Namespace: service.Namespace, + Annotations: map[string]string{ + api.ServiceUIDAnnotation: string(service.UID), + api.ServiceNameAnnotation: service.Name, + }, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "v1", + Kind: "Service", + Name: service.Name, + UID: service.UID, + }, + }, + Finalizers: nil, // TODO maybe add finalizer to guarantee we never miss a delete event? + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + api.SecretDataKey: []byte(secretData), + }, + } + + _, err := secretClient.Secrets(service.Namespace).Create(context.TODO(), secret, metav1.CreateOptions{}) + if apierrors.IsAlreadyExists(err) { + actualSecret, getErr := secretClient.Secrets(service.Namespace).Get(context.TODO(), secret.Name, metav1.GetOptions{}) + if getErr != nil { + return getErr + } + + if actualSecret.Annotations[api.ServiceUIDAnnotation] != string(service.UID) { + //nolint: goerr113 + utilruntime.HandleError(fmt.Errorf("secret %s/%s does not have corresponding service UID %v", actualSecret.Namespace, actualSecret.Name, service.UID)) + return nil // drop from queue because we cannot safely update this secret + } + + klog.V(4).InfoS("updating data in existing secret", "namespace", secret.Namespace, "name", secret.Name) + // Actually update the secret in the regeneration case (the secret already exists but we want to update to new secretData). + _, updateErr := secretClient.Secrets(secret.Namespace).Update(context.TODO(), secret, metav1.UpdateOptions{}) + return updateErr + } + if err != nil { + return fmt.Errorf("unable to create secret %s/%s: %w", secret.Namespace, secret.Name, err) + } + + return nil + } + + syncer := controllerlib.SyncFunc(func(ctx controllerlib.Context) error { + service, err := serviceLister.Services(ctx.Key.Namespace).Get(ctx.Key.Name) + if apierrors.IsNotFound(err) { + return nil + } + if err != nil { + return fmt.Errorf("unable to get the service %s/%s: %w", service.Namespace, service.Name, err) + } + + ok, err := requiresSecretGeneration(service) + if err != nil || !ok { + return err + } + + return generateSecret(service) + }) + + config := controllerlib.Config{ + Name: "example-controller-creating", + Syncer: syncer, + } + + toServiceName := func(secret *corev1.Secret) (string, bool) { + serviceName := secret.Annotations[api.ServiceNameAnnotation] + return serviceName, len(serviceName) != 0 + } + + hasSecretNameAnnotation := func(obj metav1.Object) bool { + return len(obj.GetAnnotations()[api.SecretNameAnnotation]) != 0 + } + hasSecretNameAnnotationUpdate := func(oldObj, newObj metav1.Object) bool { + return hasSecretNameAnnotation(newObj) || hasSecretNameAnnotation(oldObj) + } + + return controllerlib.New(config, + controllerlib.WithInformer(services, controllerlib.FilterFuncs{ + AddFunc: hasSecretNameAnnotation, + UpdateFunc: hasSecretNameAnnotationUpdate, + }, controllerlib.InformerOption{}), + + controllerlib.WithInformer(secrets, controllerlib.FilterFuncs{ + ParentFunc: func(obj metav1.Object) controllerlib.Key { + secret := obj.(*corev1.Secret) + serviceName, _ := toServiceName(secret) + return controllerlib.Key{Namespace: secret.Namespace, Name: serviceName} + }, + DeleteFunc: func(obj metav1.Object) bool { + secret := obj.(*corev1.Secret) + serviceName, ok := toServiceName(secret) + if !ok { + return false + } + service, err := serviceLister.Services(secret.Namespace).Get(serviceName) + if apierrors.IsNotFound(err) { + return false + } + if err != nil { + utilruntime.HandleError(fmt.Errorf("unable to get service %s/%s: %w", secret.Namespace, serviceName, err)) + return false + } + klog.V(4).InfoS("recreating secret", "namespace", service.Namespace, "name", service.Name) + return true + }, + }, controllerlib.InformerOption{}), + + controllerlib.WithRecorder(recorder), // TODO actually use the recorder + ) +} diff --git a/internal/controllerlib/test/integration/examplecontroller/controller/creating_test.go b/internal/controllerlib/test/integration/examplecontroller/controller/creating_test.go new file mode 100644 index 00000000..9abfb1b7 --- /dev/null +++ b/internal/controllerlib/test/integration/examplecontroller/controller/creating_test.go @@ -0,0 +1,170 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package controller + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes/fake" + coretesting "k8s.io/client-go/testing" + "k8s.io/client-go/tools/events" + + "github.com/suzerain-io/pinniped/internal/controllerlib" + "github.com/suzerain-io/pinniped/internal/controllerlib/test/integration/examplecontroller/api" +) + +func TestNewExampleCreatingController(t *testing.T) { + secretsGVR := schema.GroupVersionResource{Version: "v1", Resource: "secrets"} + + type args struct { + services []*corev1.Service + secrets []*corev1.Secret + secretData string + } + type keyErr struct { + key controllerlib.Key + err error + } + tests := []struct { + name string + args args + wantActions []coretesting.Action + wantKeyErrs []keyErr + }{ + { + name: "service has annotation but secret does not exist", + args: args{ + services: []*corev1.Service{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns-1", + Name: "service-1", + Annotations: map[string]string{ + api.SecretNameAnnotation: "secret-1", + }, + UID: "0001", + }, + }, + }, + secretData: "foo-secret-1", + }, + wantKeyErrs: []keyErr{ + { + key: controllerlib.Key{ + Namespace: "ns-1", + Name: "service-1", + }, + err: nil, // we expect no error with this key + }, + }, + wantActions: []coretesting.Action{ + coretesting.NewCreateAction(secretsGVR, "ns-1", &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secret-1", + Namespace: "ns-1", + Annotations: map[string]string{ + api.ServiceUIDAnnotation: "0001", + api.ServiceNameAnnotation: "service-1", + }, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "v1", + Kind: "Service", + Name: "service-1", + UID: "0001", + }, + }, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + api.SecretDataKey: []byte("foo-secret-1"), + }, + }), + }, + }, + } + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + kubeClient := fake.NewSimpleClientset() + for i := range tt.args.services { + service := tt.args.services[i] + err := kubeClient.Tracker().Add(service) + require.NoError(t, err) + } + for i := range tt.args.secrets { + secret := tt.args.secrets[i] + err := kubeClient.Tracker().Add(secret) + require.NoError(t, err) + } + + recorder := events.NewEventBroadcasterAdapter(kubeClient).NewRecorder("example-controller") + kubeInformers := informers.NewSharedInformerFactory(kubeClient, 0) + + creatingController := NewExampleCreatingController( + kubeInformers.Core().V1().Services(), + kubeInformers.Core().V1().Secrets(), + kubeClient.CoreV1(), + recorder, + tt.args.secretData, + ) + + keyErrs := make(chan keyErr) + controllerlib.TestWrap(t, creatingController, func(syncer controllerlib.Syncer) controllerlib.Syncer { + return controllerlib.SyncFunc(func(ctx controllerlib.Context) error { + err := syncer.Sync(ctx) + + keyErrs <- keyErr{ + key: ctx.Key, + err: err, + } + + return err + }) + }) + + // a different approach would be to use TestSync and run each iteration manually: + // + // err := controller.TestSync(t, c, ...) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + kubeInformers.Start(ctx.Done()) + go creatingController.Run(ctx, 5) // TODO maybe only use one worker? + + var actualKeyErrs []keyErr + done: + for { + select { + case key := <-keyErrs: + actualKeyErrs = append(actualKeyErrs, key) + + case <-time.After(3 * time.Second): + // this assumes that calls to Sync are never more than three seconds apart + // we have five workers so there is little chance they all hang around doing nothing for that long + break done + } + } + + // TODO: Figure out how to capture actions from informers + // TODO: I think we need some more fancy order independent equal comparison here + + require.Equal(t, tt.wantKeyErrs, actualKeyErrs) + + // ignore the discovery call from the event recorder and the list/watch from both informers (first five events) + require.Equal(t, tt.wantActions, kubeClient.Actions()[5:]) + }) + } +} diff --git a/internal/controllerlib/test/integration/examplecontroller/controller/updating.go b/internal/controllerlib/test/integration/examplecontroller/controller/updating.go new file mode 100644 index 00000000..ec7b2210 --- /dev/null +++ b/internal/controllerlib/test/integration/examplecontroller/controller/updating.go @@ -0,0 +1,149 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package controller + +import ( + "context" + "fmt" + "reflect" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + corev1informers "k8s.io/client-go/informers/core/v1" + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/tools/events" + + "github.com/suzerain-io/pinniped/internal/controllerlib" + "github.com/suzerain-io/pinniped/internal/controllerlib/test/integration/examplecontroller/api" +) + +func NewExampleUpdatingController( + services corev1informers.ServiceInformer, + secrets corev1informers.SecretInformer, + secretClient corev1client.SecretsGetter, + recorder events.EventRecorder, + secretData string, +) controllerlib.Controller { + serviceLister := services.Lister() + secretLister := secrets.Lister() + + // note that these functions do not need to be inlined + // this just demonstrates that for simple Syncer implementations, everything can be in one place + + toServiceName := func(secret *corev1.Secret) (string, bool) { + serviceName := secret.Annotations[api.ServiceNameAnnotation] + return serviceName, len(serviceName) != 0 + } + + ensureSecretData := func(service *corev1.Service, secretCopy *corev1.Secret) bool { + var needsUpdate bool + + expectedData := map[string][]byte{ + api.SecretDataKey: []byte(secretData), + } + if !reflect.DeepEqual(secretCopy.Data, expectedData) { + secretCopy.Data = expectedData + needsUpdate = true + } + + expectedOwnerReferences := []metav1.OwnerReference{ + { + APIVersion: "v1", + Kind: "Service", + Name: service.Name, + UID: service.UID, + }, + } + if !reflect.DeepEqual(secretCopy.OwnerReferences, expectedOwnerReferences) { + secretCopy.OwnerReferences = expectedOwnerReferences + needsUpdate = true + } + + return needsUpdate + } + + isSecretValidForService := func(service *corev1.Service, secret *corev1.Secret) bool { + if service.Annotations[api.SecretNameAnnotation] != secret.Name { + return false + } + if secret.Annotations[api.ServiceUIDAnnotation] != string(service.UID) { + return false + } + return true + } + + getServiceForSecret := func(secret *corev1.Secret) (*corev1.Service, error) { + serviceName, ok := toServiceName(secret) + if !ok { + return nil, nil + } + service, err := serviceLister.Services(secret.Namespace).Get(serviceName) + if apierrors.IsNotFound(err) { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("unable to get service %s/%s: %w", secret.Namespace, serviceName, err) + } + return service, nil + } + + syncer := controllerlib.SyncFunc(func(ctx controllerlib.Context) error { + secret, err := secretLister.Secrets(ctx.Key.Namespace).Get(ctx.Key.Name) + if apierrors.IsNotFound(err) { + return nil + } + if err != nil { + return fmt.Errorf("unable to get the secret %s/%s: %w", secret.Namespace, secret.Name, err) + } + + service, err := getServiceForSecret(secret) + if err != nil || service == nil { + return err + } + + if !isSecretValidForService(service, secret) { + //nolint: goerr113 + utilruntime.HandleError(fmt.Errorf("secret %s/%s does not have corresponding service UID %v", secret.Namespace, secret.Name, service.UID)) + return nil + } + + // make a copy to avoid mutating cache state + secretCopy := secret.DeepCopy() + + if needsUpdate := ensureSecretData(service, secretCopy); needsUpdate { + _, updateErr := secretClient.Secrets(secretCopy.Namespace).Update(context.TODO(), secretCopy, metav1.UpdateOptions{}) + return updateErr + } + + return nil + }) + + config := controllerlib.Config{ + Name: "example-controller-updating", + Syncer: syncer, + } + + addSecret := func(obj metav1.Object) bool { + secret := obj.(*corev1.Secret) + _, ok := toServiceName(secret) + return ok + } + + return controllerlib.New(config, + controllerlib.WithInformer(services, controllerlib.FilterFuncs{}, controllerlib.InformerOption{SkipEvents: true}), + + controllerlib.WithInformer(secrets, controllerlib.FilterFuncs{ + AddFunc: addSecret, + UpdateFunc: func(oldObj, newObj metav1.Object) bool { + return addSecret(newObj) || addSecret(oldObj) + }, + }, controllerlib.InformerOption{}), + + controllerlib.WithRecorder(recorder), // TODO actually use the recorder + ) +} diff --git a/internal/controllerlib/test/integration/examplecontroller/starter/starter.go b/internal/controllerlib/test/integration/examplecontroller/starter/starter.go new file mode 100644 index 00000000..ec8ae3a5 --- /dev/null +++ b/internal/controllerlib/test/integration/examplecontroller/starter/starter.go @@ -0,0 +1,56 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package starter + +import ( + "context" + "fmt" + "time" + + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/events" + + "github.com/suzerain-io/pinniped/internal/controllerlib" + examplecontroller "github.com/suzerain-io/pinniped/internal/controllerlib/test/integration/examplecontroller/controller" +) + +func StartExampleController(ctx context.Context, config *rest.Config, secretData string) error { + kubeClient, err := kubernetes.NewForConfig(config) + if err != nil { + return fmt.Errorf("failed to build client: %w", err) + } + + kubeInformers := informers.NewSharedInformerFactory(kubeClient, 20*time.Minute) + + recorder := events.NewEventBroadcasterAdapter(kubeClient).NewRecorder("example-controller") + + manager := controllerlib.NewManager(). + WithController( + examplecontroller.NewExampleCreatingController( + kubeInformers.Core().V1().Services(), + kubeInformers.Core().V1().Secrets(), + kubeClient.CoreV1(), + recorder, + secretData, + ), 5, + ). + WithController( + examplecontroller.NewExampleUpdatingController( + kubeInformers.Core().V1().Services(), + kubeInformers.Core().V1().Secrets(), + kubeClient.CoreV1(), + recorder, + secretData, + ), 5, + ) + + kubeInformers.Start(ctx.Done()) + go manager.Start(ctx) + + return nil +} diff --git a/internal/controllerlib/test/integration/examplecontroller_test.go b/internal/controllerlib/test/integration/examplecontroller_test.go new file mode 100644 index 00000000..e9be9412 --- /dev/null +++ b/internal/controllerlib/test/integration/examplecontroller_test.go @@ -0,0 +1,161 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package integration + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/watch" + + "github.com/suzerain-io/pinniped/internal/controllerlib/test/integration/examplecontroller/api" + examplestart "github.com/suzerain-io/pinniped/internal/controllerlib/test/integration/examplecontroller/starter" + "github.com/suzerain-io/pinniped/internal/controllerlib/test/library" +) + +func TestExampleController(t *testing.T) { + config := library.NewClientConfig(t) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + secretData := "super-secret-data-1" + + err := examplestart.StartExampleController(ctx, config, secretData) + require.NoError(t, err) + + client := library.NewClientset(t) + + namespaces := client.CoreV1().Namespaces() + + namespace, err := namespaces.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "example-controller-test-", + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + defer func() { + deleteErr := namespaces.Delete(context.Background(), namespace.Name, metav1.DeleteOptions{}) + require.NoError(t, deleteErr) + }() + + services := client.CoreV1().Services(namespace.Name) + secrets := client.CoreV1().Secrets(namespace.Name) + + secretsWatch, err := secrets.Watch(context.Background(), metav1.ListOptions{}) + require.NoError(t, err) + defer secretsWatch.Stop() + + service := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-service-test", + Annotations: map[string]string{ + api.SecretNameAnnotation: "example-secret-name", + }, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Port: 443, + }, + }, + }, + } + _, err = services.Create(ctx, service, metav1.CreateOptions{}) + require.NoError(t, err) + + timeout := time.After(10 * time.Second) +done: + for { + select { + case event := <-secretsWatch.ResultChan(): + if event.Type != watch.Added { + continue + } + secret, ok := event.Object.(*corev1.Secret) + if !ok { + continue + } + if secret.Name != service.Annotations[api.SecretNameAnnotation] { + continue + } + + expectedData := map[string][]byte{ + api.SecretDataKey: []byte(secretData), + } + require.Equal(t, expectedData, secret.Data, "expected to see new secret data: %s", library.Sdump(secret)) + break done // immediately stop consuming events because we want to check for updated events below + + case <-timeout: + t.Fatal("timed out waiting to see new secret") + } + } + + // shutdown the controllers so we can change the secret data + cancel() + time.Sleep(5 * time.Second) // wait a bit for the controllers to shut down + + ctx, cancel = context.WithCancel(context.Background()) + defer cancel() + + secretData2 := "super-secret-data-2" + + err = examplestart.StartExampleController(ctx, config, secretData2) + require.NoError(t, err) + + timeout = time.After(10 * time.Second) +done2: + for { + select { + case event := <-secretsWatch.ResultChan(): + if event.Type != watch.Modified { + continue + } + secret, ok := event.Object.(*corev1.Secret) + if !ok { + continue + } + if secret.Name != service.Annotations[api.SecretNameAnnotation] { + continue + } + + expectedData := map[string][]byte{ + api.SecretDataKey: []byte(secretData2), + } + require.Equal(t, expectedData, secret.Data, "expected to see updated secret data: %s", library.Sdump(secret)) + break done2 // immediately stop consuming events because we want to check for hot loops below + + case <-timeout: + t.Fatal("timed out waiting to see updated secret") + } + } + + timeout = time.After(5 * time.Second) +done3: + for { + select { + case event := <-secretsWatch.ResultChan(): + secret, ok := event.Object.(*corev1.Secret) + if !ok { + continue + } + if secret.Name != service.Annotations[api.SecretNameAnnotation] { + continue + } + + // this assumes that no other actor in the system is trying to mutate this secret + t.Errorf("unexpected event seen for secret: %s", library.Sdump(event)) + + case <-timeout: + break done3 // we saw no events matching our secret meaning that we are not hot looping + } + } +} diff --git a/internal/controllerlib/test/integration/main_test.go b/internal/controllerlib/test/integration/main_test.go new file mode 100644 index 00000000..e80a4d90 --- /dev/null +++ b/internal/controllerlib/test/integration/main_test.go @@ -0,0 +1,32 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package integration + +import ( + "fmt" + "os" + "strconv" + "testing" +) + +// force users to opt-in to running the integration tests. +// this prevents them from running if someone does `go test ./...` +// these tests could be destructive to the cluster under test. +const magicIntegrationTestsEnvVar = "CONTROLLER_GO_TEST_INTEGRATION" + +var shouldRunIntegrationTests = func() bool { + b, _ := strconv.ParseBool(os.Getenv(magicIntegrationTestsEnvVar)) + return b +}() + +func TestMain(m *testing.M) { + if !shouldRunIntegrationTests { + fmt.Printf("SKIP: %s=true env var must be explicitly set for integration tests to run\n", magicIntegrationTestsEnvVar) + os.Exit(0) + } + + os.Exit(m.Run()) +} diff --git a/internal/controllerlib/test/library/client.go b/internal/controllerlib/test/library/client.go new file mode 100644 index 00000000..0883722a --- /dev/null +++ b/internal/controllerlib/test/library/client.go @@ -0,0 +1,57 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package library + +import ( + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" +) + +func NewClientConfig(t *testing.T) *rest.Config { + t.Helper() + + return newClientConfigWithOverrides(t, &clientcmd.ConfigOverrides{}) +} + +func NewClientConfigWithCertAndKey(t *testing.T, cert, key string) *rest.Config { + t.Helper() + + return newClientConfigWithOverrides(t, &clientcmd.ConfigOverrides{ + AuthInfo: clientcmdapi.AuthInfo{ + ClientCertificateData: []byte(cert), + ClientKeyData: []byte(key), + }, + }) +} + +func newClientConfigWithOverrides(t *testing.T, overrides *clientcmd.ConfigOverrides) *rest.Config { + t.Helper() + + loader := clientcmd.NewDefaultClientConfigLoadingRules() + clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loader, overrides) + config, err := clientConfig.ClientConfig() + require.NoError(t, err) + return config +} + +func NewClientset(t *testing.T) kubernetes.Interface { + t.Helper() + + return NewClientsetWithConfig(t, NewClientConfig(t)) +} + +func NewClientsetWithConfig(t *testing.T, config *rest.Config) kubernetes.Interface { + t.Helper() + + result, err := kubernetes.NewForConfig(config) + require.NoError(t, err, "unexpected failure from kubernetes.NewForConfig()") + return result +} diff --git a/internal/controllerlib/test/library/spew.go b/internal/controllerlib/test/library/spew.go new file mode 100644 index 00000000..2df7b088 --- /dev/null +++ b/internal/controllerlib/test/library/spew.go @@ -0,0 +1,23 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package library + +import "github.com/davecgh/go-spew/spew" + +func Sdump(a ...interface{}) string { + config := spew.ConfigState{ + Indent: "\t", + MaxDepth: 10, // prevent log explosion + DisableMethods: true, + DisablePointerMethods: true, + DisablePointerAddresses: true, + DisableCapacities: true, + ContinueOnMethod: true, + SortKeys: true, + SpewKeys: true, + } + return config.Sdump(a...) +} diff --git a/internal/controllerlib/testing.go b/internal/controllerlib/testing.go new file mode 100644 index 00000000..f6a96071 --- /dev/null +++ b/internal/controllerlib/testing.go @@ -0,0 +1,29 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package controllerlib + +import ( + "testing" +) + +func TestSync(t *testing.T, controller Controller, ctx Context) error { + t.Helper() // force testing import to discourage external use + return controller.sync(ctx) +} + +func TestWrap(t *testing.T, controller Controller, wrapper SyncWrapperFunc) { + t.Helper() // force testing import to discourage external use + controller.wrap(wrapper) +} + +// Just enough of the internal implementation of controller.Run() to allow +// "running" the controller without any goroutines being involved. For use +// in synchronous unit tests that wish to invoke TestSync() directly. +func TestRunSynchronously(t *testing.T, controller Controller) { + t.Helper() // force testing import to discourage external use + controller.invokeAllRunOpts() + controller.waitForCacheSyncWithTimeout() +} diff --git a/internal/controllermanager/prepare_controllers.go b/internal/controllermanager/prepare_controllers.go index a76b9eef..191cf8f0 100644 --- a/internal/controllermanager/prepare_controllers.go +++ b/internal/controllermanager/prepare_controllers.go @@ -16,11 +16,11 @@ import ( restclient "k8s.io/client-go/rest" aggregatorclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset" - "github.com/suzerain-io/controller-go" pinnipedclientset "github.com/suzerain-io/pinniped/generated/1.19/client/clientset/versioned" pinnipedinformers "github.com/suzerain-io/pinniped/generated/1.19/client/informers/externalversions" "github.com/suzerain-io/pinniped/internal/controller/apicerts" "github.com/suzerain-io/pinniped/internal/controller/issuerconfig" + "github.com/suzerain-io/pinniped/internal/controllerlib" "github.com/suzerain-io/pinniped/internal/provider" ) @@ -48,7 +48,7 @@ func PrepareControllers( createInformers(serverInstallationNamespace, k8sClient, pinnipedClient) // Create controller manager. - controllerManager := controller. + controllerManager := controllerlib. NewManager(). WithController( issuerconfig.NewPublisherController( @@ -57,7 +57,7 @@ func PrepareControllers( pinnipedClient, kubePublicNamespaceK8sInformers.Core().V1().ConfigMaps(), installationNamespacePinnipedInformers.Crd().V1alpha1().CredentialIssuerConfigs(), - controller.WithInformer, + controllerlib.WithInformer, ), singletonWorker, ). @@ -67,8 +67,8 @@ func PrepareControllers( k8sClient, aggregatorClient, installationNamespaceK8sInformers.Core().V1().Secrets(), - controller.WithInformer, - controller.WithInitialEvent, + controllerlib.WithInformer, + controllerlib.WithInitialEvent, servingCertDuration, ), singletonWorker, @@ -78,7 +78,7 @@ func PrepareControllers( serverInstallationNamespace, dynamicCertProvider, installationNamespaceK8sInformers.Core().V1().Secrets(), - controller.WithInformer, + controllerlib.WithInformer, ), singletonWorker, ). @@ -87,7 +87,7 @@ func PrepareControllers( serverInstallationNamespace, k8sClient, installationNamespaceK8sInformers.Core().V1().Secrets(), - controller.WithInformer, + controllerlib.WithInformer, servingCertRenewBefore, ), singletonWorker, diff --git a/internal/testutil/observable_with_informer_option.go b/internal/testutil/observable_with_informer_option.go index 02536a5d..6472212b 100644 --- a/internal/testutil/observable_with_informer_option.go +++ b/internal/testutil/observable_with_informer_option.go @@ -5,27 +5,27 @@ SPDX-License-Identifier: Apache-2.0 package testutil -import "github.com/suzerain-io/controller-go" +import "github.com/suzerain-io/pinniped/internal/controllerlib" type ObservableWithInformerOption struct { - informerToFilterMap map[controller.InformerGetter]controller.Filter + informerToFilterMap map[controllerlib.InformerGetter]controllerlib.Filter } func NewObservableWithInformerOption() *ObservableWithInformerOption { return &ObservableWithInformerOption{ - informerToFilterMap: make(map[controller.InformerGetter]controller.Filter), + informerToFilterMap: make(map[controllerlib.InformerGetter]controllerlib.Filter), } } func (i *ObservableWithInformerOption) WithInformer( - getter controller.InformerGetter, - filter controller.Filter, - opt controller.InformerOption, -) controller.Option { + getter controllerlib.InformerGetter, + filter controllerlib.Filter, + opt controllerlib.InformerOption, +) controllerlib.Option { i.informerToFilterMap[getter] = filter - return controller.WithInformer(getter, filter, opt) + return controllerlib.WithInformer(getter, filter, opt) } -func (i *ObservableWithInformerOption) GetFilterForInformer(getter controller.InformerGetter) controller.Filter { +func (i *ObservableWithInformerOption) GetFilterForInformer(getter controllerlib.InformerGetter) controllerlib.Filter { return i.informerToFilterMap[getter] } diff --git a/internal/testutil/observable_with_initial_event_option.go b/internal/testutil/observable_with_initial_event_option.go index c16f1c08..d7d94558 100644 --- a/internal/testutil/observable_with_initial_event_option.go +++ b/internal/testutil/observable_with_initial_event_option.go @@ -5,21 +5,21 @@ SPDX-License-Identifier: Apache-2.0 package testutil -import "github.com/suzerain-io/controller-go" +import "github.com/suzerain-io/pinniped/internal/controllerlib" type ObservableWithInitialEventOption struct { - key controller.Key + key controllerlib.Key } func NewObservableWithInitialEventOption() *ObservableWithInitialEventOption { return &ObservableWithInitialEventOption{} } -func (i *ObservableWithInitialEventOption) WithInitialEvent(key controller.Key) controller.Option { +func (i *ObservableWithInitialEventOption) WithInitialEvent(key controllerlib.Key) controllerlib.Option { i.key = key - return controller.WithInitialEvent(key) + return controllerlib.WithInitialEvent(key) } -func (i *ObservableWithInitialEventOption) GetInitialEventKey() controller.Key { +func (i *ObservableWithInitialEventOption) GetInitialEventKey() controllerlib.Key { return i.key }