├── .gitignore ├── LICENSE ├── Makefile ├── alias.go ├── components ├── random_secret.go ├── random_secret_test.go ├── ready_status.go ├── ready_status_test.go ├── suite_test.go ├── template.go ├── template_test.go ├── test_crds │ └── test.coderanger.net_testobjects.yaml ├── test_templates │ ├── configmap.yml │ └── deployment.yml └── testobject_test.go ├── conditions ├── conditions.go ├── deepcopy.go └── types.go ├── core ├── components.go ├── conditions.go ├── context.go └── reconciler.go ├── go.mod ├── go.sum ├── predicates ├── deep_equals.go ├── secret_field.go └── update_debug.go ├── randstring └── randstring.go ├── templates ├── filesystem.go ├── filesystem_test.go ├── templates.go ├── templates_suite_test.go ├── templates_test.go └── test_templates │ ├── empty.yml │ ├── helpers │ └── deployment.yml.tpl │ ├── test.txt │ ├── test1.yml.tpl │ ├── test2.yml.tpl │ └── test3.yml.tpl └── tests ├── client.go ├── functional.go ├── matchers └── conditions.go └── unit.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.coverprofile 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: test 2 | 3 | ifdef CI 4 | CI_GINKGO_ARGS=--v -compilers 4 5 | else 6 | CI_GINKGO_ARGS= 7 | endif 8 | 9 | # Run tests 10 | test: fmt vet 11 | ginkgo --randomizeAllSpecs --randomizeSuites --cover --trace --progress ${GINKGO_ARGS} ${CI_GINKGO_ARGS} -r ./... 12 | gover 13 | 14 | # Run go fmt against code 15 | fmt: 16 | go fmt ./... 17 | 18 | # Run go vet against code 19 | vet: 20 | go vet ./... 21 | 22 | # Display a coverage report 23 | cover: 24 | go tool cover -html=gover.coverprofile 25 | -------------------------------------------------------------------------------- /alias.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Noah Kantrowitz 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllerutils 18 | 19 | import ( 20 | _ "github.com/coderanger/controller-utils/components" 21 | "github.com/coderanger/controller-utils/core" 22 | "github.com/coderanger/controller-utils/tests" 23 | ) 24 | 25 | type Component = core.Component 26 | 27 | type Context = core.Context 28 | 29 | type Reconciler = core.Reconciler 30 | 31 | type Result = core.Result 32 | 33 | type UnitSuiteHelper = tests.UnitSuiteHelper 34 | 35 | type UnitHelper = tests.UnitHelper 36 | 37 | type FunctionalSuiteHelper = tests.FunctionalSuiteHelper 38 | 39 | type FunctionalHelper = tests.FunctionalHelper 40 | 41 | var ( 42 | NewReconciler = core.NewReconciler 43 | 44 | Unit = tests.Unit 45 | 46 | Functional = tests.Functional 47 | ) 48 | -------------------------------------------------------------------------------- /components/random_secret.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Noah Kantrowitz 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package components 18 | 19 | import ( 20 | "encoding/base64" 21 | "fmt" 22 | "strings" 23 | 24 | "github.com/pkg/errors" 25 | corev1 "k8s.io/api/core/v1" 26 | kerrors "k8s.io/apimachinery/pkg/api/errors" 27 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 28 | "k8s.io/apimachinery/pkg/runtime/schema" 29 | "k8s.io/apimachinery/pkg/types" 30 | ctrl "sigs.k8s.io/controller-runtime" 31 | "sigs.k8s.io/controller-runtime/pkg/builder" 32 | "sigs.k8s.io/controller-runtime/pkg/client" 33 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 34 | 35 | "github.com/coderanger/controller-utils/core" 36 | "github.com/coderanger/controller-utils/predicates" 37 | "github.com/coderanger/controller-utils/randstring" 38 | ) 39 | 40 | const RANDOM_BYTES = 32 41 | 42 | // Lossy base64 endcoding to make passwords that will work basically anywhere. 43 | var RandEncoding = base64.NewEncoding("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl").WithPadding(base64.NoPadding) 44 | 45 | type randomSecretComponent struct { 46 | name string 47 | keys []string 48 | } 49 | 50 | func NewRandomSecretComponent(name string, keys ...string) core.Component { 51 | if len(keys) == 0 { 52 | // Default key if none are specified. 53 | keys = []string{"password"} 54 | } 55 | return &randomSecretComponent{name, keys} 56 | } 57 | 58 | func (comp *randomSecretComponent) Setup(_ *core.Context, bldr *ctrl.Builder) error { 59 | bldr.Owns(&corev1.Secret{}, builder.WithPredicates(predicates.SecretField(comp.keys))) 60 | return nil 61 | } 62 | 63 | func (comp *randomSecretComponent) Reconcile(ctx *core.Context) (core.Result, error) { 64 | name := comp.name 65 | if strings.Contains(name, "%s") { 66 | name = fmt.Sprintf(name, ctx.Object.GetName()) 67 | } 68 | 69 | secretName := types.NamespacedName{ 70 | Name: name, 71 | Namespace: ctx.Object.GetNamespace(), 72 | } 73 | existingSecret := &corev1.Secret{} 74 | // Use the uncached client to avoid race conditions. 75 | err := ctx.UncachedClient.Get(ctx, secretName, existingSecret) 76 | if err != nil { 77 | if kerrors.IsNotFound(err) { 78 | // Patch will create it so no need for anything else specific. 79 | } else { 80 | return core.Result{}, errors.Wrapf(err, "error getting secret %s", secretName) 81 | } 82 | } 83 | 84 | data := map[string][]byte{} 85 | 86 | changed := false 87 | for _, key := range comp.keys { 88 | val, ok := existingSecret.Data[key] 89 | if !ok || len(val) == 0 { 90 | val, err = randstring.RandomBytes(RANDOM_BYTES) 91 | if err != nil { 92 | return core.Result{}, errors.Wrap(err, "error generating random bytes") 93 | } 94 | ctx.Events.Eventf(ctx.Object, "Normal", "GeneratedRandomValue", "Generated a random value for key %s", key) 95 | changed = true 96 | } 97 | data[key] = val 98 | // Store the values into context for use by later components. 99 | ctx.Data[key] = string(val) 100 | } 101 | 102 | if changed { 103 | secret := &unstructured.Unstructured{ 104 | Object: map[string]interface{}{ 105 | "type": "Opaque", 106 | "data": data, 107 | }, 108 | } 109 | secret.SetName(secretName.Name) 110 | secret.SetNamespace(secretName.Namespace) 111 | secret.SetGroupVersionKind(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Secret"}) 112 | 113 | err = controllerutil.SetControllerReference(ctx.Object, secret, ctx.Scheme) 114 | if err != nil { 115 | return core.Result{}, errors.Wrap(err, "error setting owner reference") 116 | } 117 | 118 | // Sigh *bool. 119 | force := true 120 | err = ctx.Client.Patch(ctx, secret, client.Apply, &client.PatchOptions{Force: &force, FieldManager: ctx.FieldManager}) 121 | if err != nil { 122 | return core.Result{}, errors.Wrapf(err, "error applying secret %s", secretName) 123 | } 124 | } 125 | 126 | return core.Result{}, nil 127 | } 128 | 129 | func init() { 130 | // Avoid import loops. 131 | core.NewRandomSecretComponent = NewRandomSecretComponent 132 | } 133 | -------------------------------------------------------------------------------- /components/random_secret_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Noah Kantrowitz 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package components 18 | 19 | import ( 20 | "context" 21 | 22 | . "github.com/onsi/ginkgo" 23 | . "github.com/onsi/gomega" 24 | corev1 "k8s.io/api/core/v1" 25 | kerrors "k8s.io/apimachinery/pkg/api/errors" 26 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 | "k8s.io/apimachinery/pkg/types" 28 | 29 | "github.com/coderanger/controller-utils/core" 30 | "github.com/coderanger/controller-utils/tests" 31 | ) 32 | 33 | type exposeDataComponent struct { 34 | namespace string 35 | dest *core.ContextData 36 | } 37 | 38 | func (comp *exposeDataComponent) Reconcile(ctx *core.Context) (core.Result, error) { 39 | metaObj := ctx.Object.(metav1.Object) 40 | if comp.namespace == metaObj.GetNamespace() { 41 | *comp.dest = ctx.Data 42 | } 43 | return core.Result{}, nil 44 | } 45 | 46 | var _ = Describe("RandomSecret component", func() { 47 | var helper *tests.FunctionalHelper 48 | var obj *TestObject 49 | var readyStatusComp core.Component 50 | 51 | BeforeEach(func() { 52 | obj = &TestObject{ 53 | ObjectMeta: metav1.ObjectMeta{Name: "testing"}, 54 | } 55 | readyStatusComp = NewReadyStatusComponent() 56 | }) 57 | 58 | AfterEach(func() { 59 | if helper != nil { 60 | helper.MustStop() 61 | } 62 | helper = nil 63 | }) 64 | 65 | It("creates a secret", func() { 66 | var contextData core.ContextData 67 | exposeDataComp := &exposeDataComponent{dest: &contextData} 68 | comp := NewRandomSecretComponent("random", "key") 69 | helper = startTestController(comp, exposeDataComp, readyStatusComp) 70 | exposeDataComp.namespace = helper.Namespace 71 | c := helper.TestClient 72 | 73 | c.Create(obj) 74 | c.EventuallyGetName(obj.Name, obj, c.EventuallyCondition("Ready", "True")) 75 | 76 | secret := &corev1.Secret{} 77 | c.GetName("random", secret) 78 | Expect(secret.Data).To(HaveKeyWithValue("key", HaveLen(43))) 79 | Expect(contextData).To(HaveKeyWithValue("key", BeEquivalentTo(secret.Data["key"]))) 80 | }) 81 | 82 | It("uses password as the default key", func() { 83 | var contextData core.ContextData 84 | exposeDataComp := &exposeDataComponent{dest: &contextData} 85 | comp := NewRandomSecretComponent("random") 86 | helper = startTestController(comp, exposeDataComp, readyStatusComp) 87 | exposeDataComp.namespace = helper.Namespace 88 | c := helper.TestClient 89 | 90 | c.Create(obj) 91 | c.EventuallyGetName(obj.Name, obj, c.EventuallyCondition("Ready", "True")) 92 | 93 | secret := &corev1.Secret{} 94 | c.GetName("random", secret) 95 | Expect(secret.Data).To(HaveKeyWithValue("password", HaveLen(43))) 96 | Expect(contextData).To(HaveKeyWithValue("password", BeEquivalentTo(secret.Data["password"]))) 97 | }) 98 | 99 | It("creates multiple keys", func() { 100 | var contextData core.ContextData 101 | exposeDataComp := &exposeDataComponent{dest: &contextData} 102 | comp := NewRandomSecretComponent("random", "key1", "key2") 103 | helper = startTestController(comp, exposeDataComp, readyStatusComp) 104 | exposeDataComp.namespace = helper.Namespace 105 | c := helper.TestClient 106 | 107 | c.Create(obj) 108 | c.EventuallyGetName(obj.Name, obj, c.EventuallyCondition("Ready", "True")) 109 | 110 | secret := &corev1.Secret{} 111 | c.GetName("random", secret) 112 | Expect(secret.Data).To(HaveKeyWithValue("key1", HaveLen(43))) 113 | Expect(contextData).To(HaveKeyWithValue("key1", BeEquivalentTo(secret.Data["key1"]))) 114 | Expect(secret.Data).To(HaveKeyWithValue("key2", HaveLen(43))) 115 | Expect(contextData).To(HaveKeyWithValue("key2", BeEquivalentTo(secret.Data["key2"]))) 116 | Expect(secret.Data["key1"]).ToNot(Equal(secret.Data["key2"])) 117 | }) 118 | 119 | It("handles a dynamic name", func() { 120 | var contextData core.ContextData 121 | exposeDataComp := &exposeDataComponent{dest: &contextData} 122 | comp := NewRandomSecretComponent("%s-random", "key") 123 | helper = startTestController(comp, exposeDataComp, readyStatusComp) 124 | exposeDataComp.namespace = helper.Namespace 125 | c := helper.TestClient 126 | 127 | c.Create(obj) 128 | c.EventuallyGetName(obj.Name, obj, c.EventuallyCondition("Ready", "True")) 129 | 130 | secret := &corev1.Secret{} 131 | c.EventuallyGetName("testing-random", secret) 132 | Expect(secret.Data).To(HaveKeyWithValue("key", HaveLen(43))) 133 | Expect(contextData).To(HaveKeyWithValue("key", BeEquivalentTo(secret.Data["key"]))) 134 | }) 135 | 136 | It("updates an existing blank secret", func() { 137 | var contextData core.ContextData 138 | exposeDataComp := &exposeDataComponent{dest: &contextData} 139 | comp := NewRandomSecretComponent("random", "key") 140 | helper = startTestController(comp, exposeDataComp, readyStatusComp) 141 | exposeDataComp.namespace = helper.Namespace 142 | c := helper.TestClient 143 | 144 | preSecret := &corev1.Secret{ 145 | ObjectMeta: metav1.ObjectMeta{Name: "random"}, 146 | } 147 | c.Create(preSecret) 148 | 149 | c.Create(obj) 150 | c.EventuallyGetName(obj.Name, obj, c.EventuallyCondition("Ready", "True")) 151 | 152 | secret := &corev1.Secret{} 153 | c.GetName("random", secret) 154 | Expect(secret.Data).To(HaveKeyWithValue("key", HaveLen(43))) 155 | Expect(contextData).To(HaveKeyWithValue("key", BeEquivalentTo(secret.Data["key"]))) 156 | }) 157 | 158 | It("updates an existing empty key", func() { 159 | var contextData core.ContextData 160 | exposeDataComp := &exposeDataComponent{dest: &contextData} 161 | comp := NewRandomSecretComponent("random", "key") 162 | helper = startTestController(comp, exposeDataComp, readyStatusComp) 163 | exposeDataComp.namespace = helper.Namespace 164 | c := helper.TestClient 165 | 166 | preSecret := &corev1.Secret{ 167 | ObjectMeta: metav1.ObjectMeta{Name: "random"}, 168 | Data: map[string][]byte{ 169 | "key": []byte(""), 170 | }, 171 | } 172 | c.Create(preSecret) 173 | 174 | c.Create(obj) 175 | c.EventuallyGetName(obj.Name, obj, c.EventuallyCondition("Ready", "True")) 176 | 177 | secret := &corev1.Secret{} 178 | c.GetName("random", secret) 179 | Expect(secret.Data).To(HaveKeyWithValue("key", HaveLen(43))) 180 | Expect(contextData).To(HaveKeyWithValue("key", BeEquivalentTo(secret.Data["key"]))) 181 | }) 182 | 183 | It("does not update other existing keys", func() { 184 | var contextData core.ContextData 185 | exposeDataComp := &exposeDataComponent{dest: &contextData} 186 | comp := NewRandomSecretComponent("random", "key") 187 | helper = startTestController(comp, exposeDataComp, readyStatusComp) 188 | exposeDataComp.namespace = helper.Namespace 189 | c := helper.TestClient 190 | 191 | preSecret := &corev1.Secret{ 192 | ObjectMeta: metav1.ObjectMeta{Name: "random"}, 193 | Data: map[string][]byte{ 194 | "other": []byte("foo"), 195 | }, 196 | } 197 | c.Create(preSecret) 198 | 199 | c.Create(obj) 200 | c.EventuallyGetName(obj.Name, obj, c.EventuallyCondition("Ready", "True")) 201 | 202 | secret := &corev1.Secret{} 203 | c.GetName("random", secret) 204 | Expect(secret.Data).To(HaveKeyWithValue("key", HaveLen(43))) 205 | Expect(contextData).To(HaveKeyWithValue("key", BeEquivalentTo(secret.Data["key"]))) 206 | Expect(secret.Data).To(HaveKeyWithValue("other", Equal([]byte("foo")))) 207 | }) 208 | 209 | It("cleans up the secret if the owner is deleted", func() { 210 | Skip("Requires controller-manager for gc controller") 211 | var contextData core.ContextData 212 | exposeDataComp := &exposeDataComponent{dest: &contextData} 213 | comp := NewRandomSecretComponent("random", "key") 214 | helper = startTestController(comp, exposeDataComp) 215 | exposeDataComp.namespace = helper.Namespace 216 | c := helper.TestClient 217 | 218 | c.Create(obj) 219 | 220 | secret := &corev1.Secret{} 221 | c.EventuallyGetName("random", secret) 222 | 223 | c.Delete(obj) 224 | 225 | Eventually(func() bool { 226 | err := helper.UncachedClient.Get(context.Background(), types.NamespacedName{Name: "random", Namespace: helper.Namespace}, secret) 227 | return kerrors.IsNotFound(err) 228 | }).Should(BeTrue()) 229 | }) 230 | }) 231 | -------------------------------------------------------------------------------- /components/ready_status.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Noah Kantrowitz 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package components 18 | 19 | import ( 20 | "strings" 21 | 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | 24 | "github.com/coderanger/controller-utils/conditions" 25 | "github.com/coderanger/controller-utils/core" 26 | "github.com/pkg/errors" 27 | ) 28 | 29 | type readyStatusComponent struct { 30 | keys []string 31 | readyConditions map[string]metav1.ConditionStatus 32 | } 33 | 34 | // Create a ReadyStatus component. Takes 0 or more conditions types. If a type 35 | // name starts with `-` then the goal is for it to be false, otherwise true. If 36 | // all requested conditions match, the Ready condition will be set to True. 37 | func NewReadyStatusComponent(keys ...string) core.Component { 38 | // Parse the keys to a map of the desired statuses so it's faster to check them in the Reconcile. 39 | readyConditions := map[string]metav1.ConditionStatus{} 40 | for _, key := range keys { 41 | target := metav1.ConditionTrue 42 | if strings.HasPrefix(key, "-") { 43 | key = key[1:] 44 | target = metav1.ConditionFalse 45 | } 46 | readyConditions[key] = target 47 | } 48 | return &readyStatusComponent{keys: keys, readyConditions: readyConditions} 49 | } 50 | 51 | func (comp *readyStatusComponent) GetReadyCondition() string { 52 | return "Ready" 53 | } 54 | 55 | func (comp *readyStatusComponent) Reconcile(ctx *core.Context) (core.Result, error) { 56 | objConditions, err := core.GetConditionsFor(ctx.Object) 57 | if err != nil { 58 | return core.Result{}, errors.Wrap(err, "error getting object conditions") 59 | } 60 | failedKeys := []string{} 61 | for conditionType, desiredStatus := range comp.readyConditions { 62 | if !conditions.IsStatusConditionPresentAndEqual(*objConditions, conditionType, desiredStatus) { 63 | failedKeys = append(failedKeys, conditionType) 64 | } 65 | } 66 | var status metav1.ConditionStatus 67 | var reason, message, messageKeys string 68 | if len(failedKeys) == 0 { 69 | status = metav1.ConditionTrue 70 | reason = "CompositeReady" 71 | message = "observed" 72 | messageKeys = strings.Join(comp.keys, ", ") 73 | } else { 74 | status = metav1.ConditionFalse 75 | reason = "CompositeNotReady" 76 | message = "did not observe" 77 | messageKeys = strings.Join(failedKeys, ", ") 78 | } 79 | // TODO The condition type should be configurable somehow. 80 | ctx.Conditions.Setf("Ready", status, reason, "ReadyStatusComponent %s correct status of %s", message, messageKeys) 81 | return core.Result{}, nil 82 | } 83 | 84 | func init() { 85 | // Avoid import loops. 86 | core.NewReadyStatusComponent = NewReadyStatusComponent 87 | } 88 | -------------------------------------------------------------------------------- /components/ready_status_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Noah Kantrowitz 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package components 18 | 19 | import ( 20 | . "github.com/onsi/ginkgo" 21 | . "github.com/onsi/gomega" 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | "sigs.k8s.io/controller-runtime/pkg/client" 24 | 25 | "github.com/coderanger/controller-utils/conditions" 26 | "github.com/coderanger/controller-utils/tests" 27 | ) 28 | 29 | var _ = Describe("ReadyStatus component", func() { 30 | var helper *tests.FunctionalHelper 31 | var obj *TestObject 32 | 33 | setCondition := func(conditionType string, status metav1.ConditionStatus) { 34 | condition := conditions.Condition{ 35 | Type: conditionType, 36 | Status: status, 37 | ObservedGeneration: obj.Generation, 38 | Reason: "Fake", 39 | } 40 | conditions.SetStatusCondition(&obj.Status.Conditions, condition) 41 | } 42 | 43 | BeforeEach(func() { 44 | obj = &TestObject{ 45 | ObjectMeta: metav1.ObjectMeta{Name: "testing"}, 46 | } 47 | }) 48 | 49 | AfterEach(func() { 50 | if CurrentGinkgoTestDescription().Failed { 51 | helper.DebugList(&TestObjectList{}) 52 | } 53 | if helper != nil { 54 | helper.MustStop() 55 | } 56 | helper = nil 57 | }) 58 | 59 | It("sets a ready status with no keys", func() { 60 | comp := NewReadyStatusComponent() 61 | helper = startTestController(comp) 62 | c := helper.TestClient 63 | 64 | c.Create(obj) 65 | c.EventuallyGetName(obj.Name, obj, c.EventuallyCondition("Ready", "True")) 66 | }) 67 | 68 | It("sets a ready status with one key", func() { 69 | comp := NewReadyStatusComponent("One") 70 | helper = startTestController(comp) 71 | c := helper.TestClient 72 | 73 | c.Create(obj) 74 | c.EventuallyGetName(obj.Name, obj, c.EventuallyCondition("Ready", "False")) 75 | 76 | // Set things to True. 77 | objClean := obj.DeepCopy() 78 | setCondition("One", metav1.ConditionTrue) 79 | c.Status().Patch(obj, client.MergeFrom(objClean)) 80 | 81 | c.EventuallyGetName(obj.Name, obj, c.EventuallyCondition("Ready", "True")) 82 | 83 | // Set them back to False. 84 | objClean = obj.DeepCopy() 85 | setCondition("One", metav1.ConditionFalse) 86 | c.Status().Patch(obj, client.MergeFrom(objClean)) 87 | 88 | c.EventuallyGetName(obj.Name, obj, c.EventuallyCondition("Ready", "False")) 89 | }) 90 | 91 | It("sets a ready status with two keys", func() { 92 | comp := NewReadyStatusComponent("One", "Two") 93 | helper = startTestController(comp) 94 | c := helper.TestClient 95 | 96 | c.Create(obj) 97 | c.EventuallyGetName(obj.Name, obj, c.EventuallyCondition("Ready", "False")) 98 | 99 | // Set things to True. 100 | objClean := obj.DeepCopy() 101 | setCondition("One", metav1.ConditionTrue) 102 | setCondition("Two", metav1.ConditionTrue) 103 | c.Status().Patch(obj, client.MergeFrom(objClean)) 104 | 105 | c.EventuallyGetName(obj.Name, obj, c.EventuallyCondition("Ready", "True")) 106 | 107 | // Set one back to False. 108 | objClean = obj.DeepCopy() 109 | setCondition("Two", metav1.ConditionFalse) 110 | c.Status().Patch(obj, client.MergeFrom(objClean)) 111 | 112 | c.EventuallyGetName(obj.Name, obj, c.EventuallyCondition("Ready", "False")) 113 | cond := conditions.FindStatusCondition(obj.Status.Conditions, "Ready") 114 | Expect(cond.Message).To(Equal("ReadyStatusComponent did not observe correct status of Two")) 115 | }) 116 | 117 | It("handles negative polarity", func() { 118 | comp := NewReadyStatusComponent("One", "-Two") 119 | helper = startTestController(comp) 120 | c := helper.TestClient 121 | 122 | c.Create(obj) 123 | c.EventuallyGetName(obj.Name, obj, c.EventuallyCondition("Ready", "False")) 124 | 125 | // Set things to True. 126 | objClean := obj.DeepCopy() 127 | setCondition("One", metav1.ConditionTrue) 128 | setCondition("Two", metav1.ConditionFalse) 129 | c.Status().Patch(obj, client.MergeFrom(objClean)) 130 | 131 | c.EventuallyGetName(obj.Name, obj, c.EventuallyCondition("Ready", "True")) 132 | 133 | // Set them back to False. 134 | objClean = obj.DeepCopy() 135 | setCondition("Two", metav1.ConditionTrue) 136 | c.Status().Patch(obj, client.MergeFrom(objClean)) 137 | 138 | c.EventuallyGetName(obj.Name, obj, c.EventuallyCondition("Ready", "False")) 139 | }) 140 | }) 141 | -------------------------------------------------------------------------------- /components/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Geomagical Labs Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package components 18 | 19 | import ( 20 | "fmt" 21 | "net/http" 22 | "testing" 23 | 24 | . "github.com/onsi/ginkgo" 25 | . "github.com/onsi/gomega" 26 | ctrl "sigs.k8s.io/controller-runtime" 27 | "sigs.k8s.io/controller-runtime/pkg/envtest/printer" 28 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 29 | 30 | "github.com/coderanger/controller-utils/core" 31 | "github.com/coderanger/controller-utils/tests" 32 | ) 33 | 34 | // Despite being unit-ish tests, these have to use the functional subsystem because there 35 | // is no fake implementation of server-side apply. 36 | var suiteHelper *tests.FunctionalSuiteHelper 37 | 38 | func TestComponents(t *testing.T) { 39 | RegisterFailHandler(Fail) 40 | 41 | RunSpecsWithDefaultAndCustomReporters(t, 42 | "Components Suite", 43 | []Reporter{printer.NewlineReporter{}}) 44 | } 45 | 46 | var _ = BeforeSuite(func(done Done) { 47 | ctrl.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter))) 48 | 49 | By("bootstrapping test environment") 50 | suiteHelper = tests.Functional(). 51 | API(TestObjectSchemeBuilder.AddToScheme). 52 | CRDPath("test_crds"). 53 | MustBuild() 54 | 55 | close(done) 56 | }, 60) 57 | 58 | var _ = AfterSuite(func() { 59 | By("tearing down the test environment") 60 | suiteHelper.MustStop() 61 | }) 62 | 63 | func newTestController(components ...core.Component) func(ctrl.Manager) error { 64 | return func(mgr ctrl.Manager) error { 65 | b := core.NewReconciler(mgr).For(&TestObject{}).Templates(http.Dir("test_templates")) 66 | for i, comp := range components { 67 | b = b.Component(fmt.Sprintf("test%d", i), comp) 68 | } 69 | return b.Complete() 70 | } 71 | } 72 | 73 | func startTestController(components ...core.Component) *tests.FunctionalHelper { 74 | helper := suiteHelper.MustStart(newTestController(components...)) 75 | ctrl.Log.WithName("suite_test").Info("Starting test controller", "test", CurrentGinkgoTestDescription().TestText, "namespace", helper.Namespace) 76 | return helper 77 | } 78 | -------------------------------------------------------------------------------- /components/template.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Noah Kantrowitz 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package components 18 | 19 | import ( 20 | "strings" 21 | 22 | "github.com/pkg/errors" 23 | kerrors "k8s.io/apimachinery/pkg/api/errors" 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 26 | "k8s.io/apimachinery/pkg/runtime" 27 | "k8s.io/apimachinery/pkg/runtime/schema" 28 | "k8s.io/apimachinery/pkg/types" 29 | ctrl "sigs.k8s.io/controller-runtime" 30 | "sigs.k8s.io/controller-runtime/pkg/builder" 31 | "sigs.k8s.io/controller-runtime/pkg/client" 32 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 33 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 34 | 35 | "github.com/coderanger/controller-utils/core" 36 | "github.com/coderanger/controller-utils/predicates" 37 | "github.com/coderanger/controller-utils/templates" 38 | ) 39 | 40 | const DELETE_ANNOTATION = "controller-utils/delete" 41 | const CONDITION_ANNOTATION = "controller-utils/condition" 42 | const DEEPEQUALS_ANNOTATION = "controller-utils/deepEquals" 43 | const SECRETFIELD_ANNOTATION = "controller-utils/secretField" 44 | 45 | type templateComponent struct { 46 | template string 47 | conditionType string 48 | } 49 | 50 | type templateData struct { 51 | Object client.Object 52 | Data map[string]interface{} 53 | } 54 | 55 | func NewTemplateComponent(template string, conditionType string) core.Component { 56 | return &templateComponent{template: template, conditionType: conditionType} 57 | } 58 | 59 | func (comp *templateComponent) GetReadyCondition() string { 60 | return comp.conditionType 61 | } 62 | 63 | func (comp *templateComponent) Setup(ctx *core.Context, bldr *ctrl.Builder) error { 64 | // Render with a fake, blank object just to find the object type. 65 | obj, err := comp.renderTemplate(ctx, true) 66 | if err != nil { 67 | return errors.Wrap(err, "error rendering setup template") 68 | } 69 | // Check if we should use the slower DeepEquals predicate. 70 | annotations := obj.GetAnnotations() 71 | deepEquals, ok := annotations[DEEPEQUALS_ANNOTATION] 72 | secretField, ok2 := annotations[SECRETFIELD_ANNOTATION] 73 | if ok && deepEquals == "true" { 74 | bldr.Owns(obj, builder.WithPredicates(predicates.DeepEquals())) 75 | } else if ok2 && secretField != "" { 76 | bldr.Owns(obj, builder.WithPredicates(predicates.SecretField(strings.Split(secretField, ",")))) 77 | } else { 78 | bldr.Owns(obj) 79 | } 80 | return nil 81 | } 82 | 83 | func (comp *templateComponent) Reconcile(ctx *core.Context) (core.Result, error) { 84 | // Render the object to an Unstructured. 85 | obj, err := comp.renderTemplate(ctx, true) 86 | if err != nil { 87 | return core.Result{}, errors.Wrap(err, "error rendering template") 88 | } 89 | 90 | // Default the namespace to the controlling object namespace. 91 | if obj.GetNamespace() == "" { 92 | obj.SetNamespace(ctx.Object.(metav1.Object).GetNamespace()) 93 | } 94 | 95 | // Check for delete annotation. 96 | annotations := obj.GetAnnotations() 97 | shouldDelete, ok := annotations[DELETE_ANNOTATION] 98 | if ok { 99 | delete(annotations, DELETE_ANNOTATION) 100 | obj.SetAnnotations(annotations) 101 | } else { 102 | shouldDelete = "false" 103 | } 104 | 105 | // Hide the DeepEquals annotation. 106 | _, ok = annotations[DEEPEQUALS_ANNOTATION] 107 | if ok { 108 | delete(annotations, DEEPEQUALS_ANNOTATION) 109 | obj.SetAnnotations(annotations) 110 | } 111 | 112 | if shouldDelete == "true" { 113 | return comp.reconcileDelete(ctx, obj) 114 | } else { 115 | return comp.reconcileCreate(ctx, obj) 116 | } 117 | } 118 | 119 | func (comp *templateComponent) renderTemplate(ctx *core.Context, unstructured bool) (client.Object, error) { 120 | return templates.Get(ctx.Templates, comp.template, unstructured, templateData{Object: ctx.Object, Data: ctx.Data}) 121 | } 122 | 123 | func (comp *templateComponent) reconcileCreate(ctx *core.Context, obj client.Object) (core.Result, error) { 124 | // Set owner reference. 125 | err := controllerutil.SetControllerReference(ctx.Object, obj, ctx.Scheme) 126 | if err != nil { 127 | return core.Result{}, errors.Wrap(err, "error setting owner reference") 128 | } 129 | 130 | // Apply the object data. 131 | force := true // Sigh *bool. 132 | err = ctx.Client.Patch(ctx, obj, client.Apply, &client.PatchOptions{Force: &force, FieldManager: ctx.FieldManager}) 133 | if err != nil { 134 | return core.Result{}, errors.Wrap(err, "error applying object") 135 | } 136 | 137 | // If we have a condition setter, check on the object status. 138 | if comp.conditionType != "" { 139 | currentObj := &unstructured.Unstructured{} 140 | currentObj.SetGroupVersionKind(obj.GetObjectKind().GroupVersionKind()) 141 | err = ctx.Client.Get(ctx, types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}, currentObj) 142 | if err != nil { 143 | return core.Result{}, errors.Wrapf(err, "error getting current object %s/%s for status", obj.GetNamespace(), obj.GetName()) 144 | } 145 | 146 | annotations := obj.GetAnnotations() 147 | if val, ok := annotations[CONDITION_ANNOTATION]; ok { 148 | status, ok := comp.getStatusFromUnstructured(currentObj, val) 149 | if ok { 150 | ctx.Conditions.Setf(comp.conditionType, status, "UpstreamConditionSet", "Upstream condition %s on %s %s was set to %s", val, obj.GetObjectKind().GroupVersionKind().Kind, obj.GetName(), status) 151 | } else { 152 | ctx.Conditions.SetfUnknown(comp.conditionType, "UpstreamConditionNotSet", "Upstream condition %s on %s %s was not set", val, obj.GetObjectKind().GroupVersionKind().Kind, obj.GetName()) 153 | } 154 | } 155 | // TODO some kind of support for an expr or CEL based option to get a status for upstream objects that don't use status conditions. 156 | } 157 | 158 | return core.Result{}, nil 159 | } 160 | 161 | func (comp *templateComponent) reconcileDelete(ctx *core.Context, obj client.Object) (core.Result, error) { 162 | currentObj := &unstructured.Unstructured{} 163 | currentObj.SetGroupVersionKind(obj.GetObjectKind().GroupVersionKind()) 164 | err := ctx.Client.Get(ctx, types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}, currentObj) 165 | if err != nil { 166 | if kerrors.IsNotFound(err) { 167 | // Didn't exist at all so we're good. 168 | if comp.conditionType != "" { 169 | ctx.Conditions.SetfTrue(comp.conditionType, "UpstreamDoesNotExist", "Upstream %s %s does not exist", obj.GetObjectKind().GroupVersionKind().Kind, obj.GetName()) 170 | } 171 | return core.Result{}, nil 172 | } 173 | return core.Result{}, errors.Wrapf(err, "error getting current object %s/%s for owner", obj.GetNamespace(), obj.GetName()) 174 | } 175 | controllerRef := metav1.GetControllerOf(currentObj) 176 | if controllerRef == nil || !comp.referSameObject(controllerRef, ctx.Object, ctx.Scheme) { 177 | // The object exists but isn't owned by this object so don't purge it. 178 | if comp.conditionType != "" { 179 | ctx.Conditions.SetfTrue(comp.conditionType, "UpstreamNotOwned", "Upstream %s %s is not owned by %s", obj.GetObjectKind().GroupVersionKind().Kind, obj.GetName(), ctx.Object.GetName()) 180 | } 181 | return core.Result{}, nil 182 | } 183 | 184 | propagation := metav1.DeletePropagationBackground 185 | err = ctx.Client.Delete(ctx, obj, &client.DeleteOptions{PropagationPolicy: &propagation}) 186 | if err != nil && !kerrors.IsNotFound(err) { 187 | return core.Result{}, errors.Wrapf(err, "error deleting %s/%s", obj.GetNamespace(), obj.GetName()) 188 | } 189 | if comp.conditionType != "" { 190 | ctx.Conditions.SetfTrue(comp.conditionType, "UpstreamDoesNotExist", "Upstream %s %s does not exist", obj.GetObjectKind().GroupVersionKind().Kind, obj.GetName()) 191 | } 192 | return core.Result{}, nil 193 | } 194 | 195 | func (comp *templateComponent) getStatusFromUnstructured(obj client.Object, srcType string) (metav1.ConditionStatus, bool) { 196 | data := obj.(*unstructured.Unstructured).UnstructuredContent() 197 | 198 | // Ooof this is ugly. Once I am set up with Expr or CEL or even a JSONPath library, try and use that instead. 199 | maybeStatus, ok := data["status"] 200 | if !ok { 201 | return metav1.ConditionUnknown, false 202 | } 203 | 204 | status, ok := maybeStatus.(map[string]interface{}) 205 | if !ok { 206 | return metav1.ConditionUnknown, false 207 | } 208 | 209 | maybeConditions, ok := status["conditions"] 210 | if !ok { 211 | return metav1.ConditionUnknown, false 212 | } 213 | 214 | conditions_, ok := maybeConditions.([]interface{}) 215 | if !ok { 216 | return metav1.ConditionUnknown, false 217 | } 218 | 219 | var status_ string 220 | for _, maybeCondition := range conditions_ { 221 | if condition_, ok := maybeCondition.(map[string]interface{}); ok { 222 | maybeType, ok := condition_["type"] 223 | maybeStatus, ok2 := condition_["status"] 224 | if ok && ok2 { 225 | type_, ok := maybeType.(string) 226 | status_, ok2 = maybeStatus.(string) 227 | if ok && ok2 && srcType == type_ { 228 | if status_ == "True" { 229 | return metav1.ConditionTrue, true 230 | } else if status_ == "False" { 231 | return metav1.ConditionFalse, true 232 | } else if status_ == "Unknown" { 233 | return metav1.ConditionUnknown, true 234 | } 235 | } 236 | } 237 | } 238 | } 239 | 240 | // Wasn't in there, we tried. 241 | return metav1.ConditionUnknown, false 242 | } 243 | 244 | // Adapted from controller-runtime. 245 | // Copyright 2018 The Kubernetes Authors. 246 | func (comp *templateComponent) referSameObject(ownerRef *metav1.OwnerReference, obj client.Object, scheme *runtime.Scheme) bool { 247 | ownerGV, err := schema.ParseGroupVersion(ownerRef.APIVersion) 248 | if err != nil { 249 | return false 250 | } 251 | 252 | objGVK, err := apiutil.GVKForObject(obj, scheme) 253 | if err != nil { 254 | return false 255 | } 256 | 257 | return ownerGV.Group == objGVK.Group && ownerRef.Kind == objGVK.Kind && ownerRef.Name == obj.GetName() 258 | } 259 | 260 | func init() { 261 | // Avoid import loops. 262 | core.NewTemplateComponent = NewTemplateComponent 263 | } 264 | -------------------------------------------------------------------------------- /components/template_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Noah Kantrowitz 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package components 18 | 19 | import ( 20 | "context" 21 | 22 | . "github.com/onsi/ginkgo" 23 | . "github.com/onsi/gomega" 24 | . "github.com/onsi/gomega/gstruct" 25 | appsv1 "k8s.io/api/apps/v1" 26 | corev1 "k8s.io/api/core/v1" 27 | kerrors "k8s.io/apimachinery/pkg/api/errors" 28 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 | "k8s.io/apimachinery/pkg/types" 30 | "sigs.k8s.io/controller-runtime/pkg/client" 31 | 32 | "github.com/coderanger/controller-utils/core" 33 | "github.com/coderanger/controller-utils/tests" 34 | ) 35 | 36 | type injectDataComponent struct { 37 | key string 38 | value string 39 | } 40 | 41 | func (comp *injectDataComponent) Reconcile(ctx *core.Context) (core.Result, error) { 42 | ctx.Data[comp.key] = comp.value 43 | return core.Result{}, nil 44 | } 45 | 46 | var _ core.Component = &injectDataComponent{} 47 | 48 | var _ = Describe("Template component", func() { 49 | var helper *tests.FunctionalHelper 50 | var obj *TestObject 51 | 52 | BeforeEach(func() { 53 | obj = &TestObject{ 54 | ObjectMeta: metav1.ObjectMeta{Name: "testing"}, 55 | } 56 | }) 57 | 58 | AfterEach(func() { 59 | if helper != nil { 60 | helper.MustStop() 61 | } 62 | helper = nil 63 | }) 64 | 65 | It("creates a deployment", func() { 66 | comp := NewTemplateComponent("deployment.yml", "") 67 | helper = startTestController(comp) 68 | c := helper.TestClient 69 | 70 | c.Create(obj) 71 | 72 | deployment := &appsv1.Deployment{} 73 | c.EventuallyGetName("testing-webserver", deployment) 74 | Expect(deployment.Spec.Replicas).To(PointTo(BeEquivalentTo(0))) 75 | Expect(deployment.Spec.Template.Spec.Containers[0].Name).To(Equal("webserver")) 76 | Expect(deployment.Spec.Template.Spec.Containers[0].Image).To(Equal("nginx")) 77 | }) 78 | 79 | It("overwrites fields controlled by the template but not others", func() { 80 | comp := NewTemplateComponent("deployment.yml", "") 81 | helper = startTestController(comp) 82 | c := helper.TestClient 83 | 84 | replicas := int32(1) 85 | preDeployment := &appsv1.Deployment{ 86 | ObjectMeta: metav1.ObjectMeta{Name: "testing-webserver"}, 87 | Spec: appsv1.DeploymentSpec{ 88 | Replicas: &replicas, 89 | MinReadySeconds: 42, 90 | Selector: &metav1.LabelSelector{ 91 | MatchLabels: map[string]string{ 92 | "app": "webserver", 93 | }, 94 | }, 95 | Template: corev1.PodTemplateSpec{ 96 | ObjectMeta: metav1.ObjectMeta{ 97 | Labels: map[string]string{ 98 | "app": "webserver", 99 | }, 100 | }, 101 | Spec: corev1.PodSpec{ 102 | Containers: []corev1.Container{ 103 | { 104 | Name: "webserver", 105 | Image: "other", 106 | }, 107 | }, 108 | }, 109 | }, 110 | }, 111 | } 112 | c.Create(preDeployment) 113 | 114 | c.Create(obj) 115 | 116 | deployment := &appsv1.Deployment{} 117 | c.EventuallyGetName("testing-webserver", deployment, c.EventuallyValue(PointTo(BeEquivalentTo(0)), func(obj client.Object) (interface{}, error) { 118 | return obj.(*appsv1.Deployment).Spec.Replicas, nil 119 | })) 120 | Expect(deployment.Spec.Replicas).To(PointTo(BeEquivalentTo(0))) 121 | Expect(deployment.Spec.Template.Spec.Containers[0].Name).To(Equal("webserver")) 122 | Expect(deployment.Spec.Template.Spec.Containers[0].Image).To(Equal("nginx")) 123 | Expect(deployment.Spec.MinReadySeconds).To(BeEquivalentTo(42)) 124 | }) 125 | 126 | It("sets a status condition", func() { 127 | comp := NewTemplateComponent("deployment.yml", "DeploymentAvailable") 128 | helper = startTestController(comp) 129 | c := helper.TestClient 130 | 131 | c.Create(obj) 132 | 133 | c.EventuallyGetName("testing", obj, c.EventuallyCondition("DeploymentAvailable", "Unknown")) 134 | 135 | deployment := &appsv1.Deployment{} 136 | c.GetName("testing-webserver", deployment) 137 | Expect(deployment.GetAnnotations()).ToNot(HaveKey(DELETE_ANNOTATION)) 138 | deploymentClean := deployment.DeepCopy() 139 | deployment.Status.Conditions = []appsv1.DeploymentCondition{ 140 | { 141 | Type: "Available", 142 | Status: corev1.ConditionFalse, 143 | Reason: "Fake", 144 | }, 145 | } 146 | c.Status().Patch(deployment, client.MergeFrom(deploymentClean)) 147 | 148 | c.EventuallyGetName("testing", obj, c.EventuallyCondition("DeploymentAvailable", "False")) 149 | 150 | c.GetName("testing-webserver", deployment) 151 | deploymentClean = deployment.DeepCopy() 152 | deployment.Status.Conditions[0].Status = corev1.ConditionTrue 153 | c.Status().Patch(deployment, client.MergeFrom(deploymentClean)) 154 | 155 | c.EventuallyGetName("testing", obj, c.EventuallyCondition("DeploymentAvailable", "True")) 156 | }) 157 | 158 | It("deletes an object", func() { 159 | comp := NewTemplateComponent("deployment.yml", "DeploymentAvailable") 160 | helper = startTestController(comp) 161 | c := helper.TestClient 162 | 163 | controller := true 164 | deployment := &appsv1.Deployment{ 165 | ObjectMeta: metav1.ObjectMeta{ 166 | Name: "testing-webserver", 167 | OwnerReferences: []metav1.OwnerReference{ 168 | { 169 | APIVersion: "test.coderanger.net/v1", 170 | Kind: "TestObject", 171 | Name: "testing", 172 | Controller: &controller, 173 | UID: "fake", // Not used but must be filled in. 174 | }, 175 | }, 176 | }, 177 | Spec: appsv1.DeploymentSpec{ 178 | Selector: &metav1.LabelSelector{ 179 | MatchLabels: map[string]string{ 180 | "app": "webserver", 181 | }, 182 | }, 183 | Template: corev1.PodTemplateSpec{ 184 | ObjectMeta: metav1.ObjectMeta{ 185 | Labels: map[string]string{ 186 | "app": "webserver", 187 | }, 188 | }, 189 | Spec: corev1.PodSpec{ 190 | Containers: []corev1.Container{ 191 | { 192 | Name: "default", 193 | Image: "something", 194 | }, 195 | }, 196 | }, 197 | }, 198 | }, 199 | } 200 | c.Create(deployment) 201 | 202 | obj.Spec.Field = "true" 203 | c.Create(obj) 204 | 205 | c.EventuallyGetName("testing", obj, c.EventuallyCondition("DeploymentAvailable", "True")) 206 | 207 | err := helper.Client.Get(context.Background(), types.NamespacedName{Name: "testing-webserver", Namespace: helper.Namespace}, deployment) 208 | Expect(err).To(HaveOccurred()) 209 | Expect(kerrors.IsNotFound(err)).To(BeTrue()) 210 | }) 211 | 212 | It("does not delete an unowned object", func() { 213 | comp := NewTemplateComponent("deployment.yml", "DeploymentAvailable") 214 | helper = startTestController(comp) 215 | c := helper.TestClient 216 | 217 | deployment := &appsv1.Deployment{ 218 | ObjectMeta: metav1.ObjectMeta{ 219 | Name: "testing-webserver", 220 | }, 221 | Spec: appsv1.DeploymentSpec{ 222 | Selector: &metav1.LabelSelector{ 223 | MatchLabels: map[string]string{ 224 | "app": "webserver", 225 | }, 226 | }, 227 | Template: corev1.PodTemplateSpec{ 228 | ObjectMeta: metav1.ObjectMeta{ 229 | Labels: map[string]string{ 230 | "app": "webserver", 231 | }, 232 | }, 233 | Spec: corev1.PodSpec{ 234 | Containers: []corev1.Container{ 235 | { 236 | Name: "default", 237 | Image: "something", 238 | }, 239 | }, 240 | }, 241 | }, 242 | }, 243 | } 244 | c.Create(deployment) 245 | 246 | obj.Spec.Field = "true" 247 | c.Create(obj) 248 | 249 | c.EventuallyGetName("testing", obj, c.EventuallyCondition("DeploymentAvailable", "True")) 250 | 251 | err := helper.Client.Get(context.Background(), types.NamespacedName{Name: "testing-webserver", Namespace: helper.Namespace}, deployment) 252 | Expect(err).ToNot(HaveOccurred()) 253 | Expect(deployment.Spec.Template.Spec.Containers[0].Image).To(Equal("something")) 254 | }) 255 | 256 | It("does not delete an object owned by someone else", func() { 257 | comp := NewTemplateComponent("deployment.yml", "DeploymentAvailable") 258 | helper = startTestController(comp) 259 | c := helper.TestClient 260 | 261 | controller := true 262 | deployment := &appsv1.Deployment{ 263 | ObjectMeta: metav1.ObjectMeta{ 264 | Name: "testing-webserver", 265 | OwnerReferences: []metav1.OwnerReference{ 266 | { 267 | APIVersion: "test.coderanger.net/v1", 268 | Kind: "TestObject", 269 | Name: "testing-other", 270 | Controller: &controller, 271 | UID: "fake", // Not used but must be filled in. 272 | }, 273 | }, 274 | }, 275 | Spec: appsv1.DeploymentSpec{ 276 | Selector: &metav1.LabelSelector{ 277 | MatchLabels: map[string]string{ 278 | "app": "webserver", 279 | }, 280 | }, 281 | Template: corev1.PodTemplateSpec{ 282 | ObjectMeta: metav1.ObjectMeta{ 283 | Labels: map[string]string{ 284 | "app": "webserver", 285 | }, 286 | }, 287 | Spec: corev1.PodSpec{ 288 | Containers: []corev1.Container{ 289 | { 290 | Name: "default", 291 | Image: "something", 292 | }, 293 | }, 294 | }, 295 | }, 296 | }, 297 | } 298 | c.Create(deployment) 299 | 300 | obj.Spec.Field = "true" 301 | c.Create(obj) 302 | 303 | c.EventuallyGetName("testing", obj, c.EventuallyCondition("DeploymentAvailable", "True")) 304 | 305 | err := helper.Client.Get(context.Background(), types.NamespacedName{Name: "testing-webserver", Namespace: helper.Namespace}, deployment) 306 | Expect(err).ToNot(HaveOccurred()) 307 | Expect(deployment.Spec.Template.Spec.Containers[0].Image).To(Equal("something")) 308 | }) 309 | 310 | It("does not delete an already deleted object", func() { 311 | comp := NewTemplateComponent("deployment.yml", "DeploymentAvailable") 312 | helper = startTestController(comp) 313 | c := helper.TestClient 314 | 315 | obj.Spec.Field = "true" 316 | c.Create(obj) 317 | 318 | c.EventuallyGetName("testing", obj, c.EventuallyCondition("DeploymentAvailable", "True")) 319 | }) 320 | 321 | It("handles template data", func() { 322 | dataComp := &injectDataComponent{key: "FOO", value: "bar"} 323 | comp := NewTemplateComponent("configmap.yml", "") 324 | helper = startTestController(dataComp, comp) 325 | c := helper.TestClient 326 | 327 | c.Create(obj) 328 | 329 | cmap := &corev1.ConfigMap{} 330 | c.EventuallyGetName("testing", cmap) 331 | Expect(cmap.Data).To(HaveKeyWithValue("FOO", Equal("bar"))) 332 | }) 333 | }) 334 | -------------------------------------------------------------------------------- /components/test_crds/test.coderanger.net_testobjects.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.2.5 7 | name: testobjects.test.coderanger.net 8 | spec: 9 | group: test.coderanger.net 10 | names: 11 | kind: TestObject 12 | listKind: TestObjectList 13 | plural: testobjects 14 | singular: testobject 15 | scope: Namespaced 16 | versions: 17 | - name: v1 18 | schema: 19 | openAPIV3Schema: 20 | properties: 21 | apiVersion: 22 | description: 'APIVersion defines the versioned schema of this representation 23 | of an object. Servers should convert recognized schemas to the latest 24 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 25 | type: string 26 | kind: 27 | description: 'Kind is a string value representing the REST resource this 28 | object represents. Servers may infer this from the endpoint the client 29 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 30 | type: string 31 | metadata: 32 | type: object 33 | spec: 34 | description: Declare a test type to as the root object for these tests. 35 | properties: 36 | field: 37 | type: string 38 | type: object 39 | status: 40 | properties: 41 | conditions: 42 | items: 43 | description: "Condition contains details for one aspect of the current 44 | state of this API Resource. --- This struct is intended for direct 45 | use as an array at the field path .status.conditions. For example, 46 | type FooStatus struct{ // Represents the observations of a foo's 47 | current state. // Known .status.conditions.type are: \"Available\", 48 | \"Progressing\", and \"Degraded\" // +patchMergeKey=type // 49 | +patchStrategy=merge // +listType=map // +listMapKey=type 50 | \ Conditions []metav1.Condition `json:\"conditions,omitempty\" 51 | patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` 52 | \n // other fields }" 53 | properties: 54 | lastTransitionTime: 55 | description: lastTransitionTime is the last time the condition 56 | transitioned from one status to another. This should be when 57 | the underlying condition changed. If that is not known, then 58 | using the time when the API field changed is acceptable. 59 | format: date-time 60 | type: string 61 | message: 62 | description: message is a human readable message indicating details 63 | about the transition. This may be an empty string. 64 | maxLength: 32768 65 | type: string 66 | observedGeneration: 67 | description: observedGeneration represents the .metadata.generation 68 | that the condition was set based upon. For instance, if .metadata.generation 69 | is currently 12, but the .status.conditions[x].observedGeneration 70 | is 9, the condition is out of date with respect to the current 71 | state of the instance. 72 | format: int64 73 | minimum: 0 74 | type: integer 75 | reason: 76 | description: reason contains a programmatic identifier indicating 77 | the reason for the condition's last transition. Producers of 78 | specific condition types may define expected values and meanings 79 | for this field, and whether the values are considered a guaranteed 80 | API. The value should be a CamelCase string. This field may 81 | not be empty. 82 | maxLength: 1024 83 | minLength: 1 84 | pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ 85 | type: string 86 | status: 87 | description: status of the condition, one of True, False, Unknown. 88 | enum: 89 | - "True" 90 | - "False" 91 | - Unknown 92 | type: string 93 | type: 94 | description: type of condition in CamelCase or in foo.example.com/CamelCase. 95 | --- Many .condition.type values are consistent across resources 96 | like Available, but because arbitrary conditions can be useful 97 | (see .node.status.conditions), the ability to deconflict is 98 | important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) 99 | maxLength: 316 100 | pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ 101 | type: string 102 | required: 103 | - lastTransitionTime 104 | - message 105 | - reason 106 | - status 107 | - type 108 | type: object 109 | type: array 110 | x-kubernetes-list-map-keys: 111 | - type 112 | x-kubernetes-list-type: map 113 | type: object 114 | type: object 115 | served: true 116 | storage: true 117 | subresources: 118 | status: {} 119 | status: 120 | acceptedNames: 121 | kind: "" 122 | plural: "" 123 | conditions: [] 124 | storedVersions: [] 125 | -------------------------------------------------------------------------------- /components/test_templates/configmap.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ .Object.Name }} 5 | data: 6 | FOO: {{ .Data.FOO }} 7 | -------------------------------------------------------------------------------- /components/test_templates/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ .Object.Name }}-webserver 5 | annotations: 6 | controller-utils/condition: Available 7 | controller-utils/delete: {{ .Object.Spec.Field | default "false" | quote }} 8 | spec: 9 | replicas: 0 10 | selector: 11 | matchLabels: 12 | app: webserver 13 | template: 14 | metadata: 15 | labels: 16 | app: webserver 17 | spec: 18 | containers: 19 | - name: webserver 20 | image: nginx 21 | -------------------------------------------------------------------------------- /components/testobject_test.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | "sigs.k8s.io/controller-runtime/pkg/scheme" 8 | 9 | "github.com/coderanger/controller-utils/conditions" 10 | ) 11 | 12 | var TestObjectSchemeBuilder = &scheme.Builder{GroupVersion: schema.GroupVersion{Group: "test.coderanger.net", Version: "v1"}} 13 | 14 | // Declare a test type as the root object for these tests. 15 | type TestObjectSpec struct { 16 | Field string `json:"field,omitempty"` 17 | } 18 | 19 | type TestObjectStatus struct { 20 | Conditions []conditions.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` 21 | } 22 | 23 | // +kubebuilder:object:root=true 24 | 25 | type TestObject struct { 26 | metav1.TypeMeta `json:",inline"` 27 | metav1.ObjectMeta `json:"metadata,omitempty"` 28 | 29 | Spec TestObjectSpec `json:"spec,omitempty"` 30 | Status TestObjectStatus `json:"status,omitempty"` 31 | } 32 | 33 | // +kubebuilder:object:root=true 34 | 35 | type TestObjectList struct { 36 | metav1.TypeMeta `json:",inline"` 37 | metav1.ListMeta `json:"metadata,omitempty"` 38 | Items []TestObject `json:"items"` 39 | } 40 | 41 | func (o *TestObject) GetConditions() *[]conditions.Condition { 42 | return &o.Status.Conditions 43 | } 44 | func init() { 45 | TestObjectSchemeBuilder.Register(&TestObject{}, &TestObjectList{}) 46 | } 47 | 48 | // Auto-generated deppcopy goop. 49 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 50 | func (in *TestObject) DeepCopyInto(out *TestObject) { 51 | *out = *in 52 | out.TypeMeta = in.TypeMeta 53 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 54 | out.Spec = in.Spec 55 | in.Status.DeepCopyInto(&out.Status) 56 | } 57 | 58 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestObject. 59 | func (in *TestObject) DeepCopy() *TestObject { 60 | if in == nil { 61 | return nil 62 | } 63 | out := new(TestObject) 64 | in.DeepCopyInto(out) 65 | return out 66 | } 67 | 68 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 69 | func (in *TestObject) DeepCopyObject() runtime.Object { 70 | if c := in.DeepCopy(); c != nil { 71 | return c 72 | } 73 | return nil 74 | } 75 | 76 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 77 | func (in *TestObjectList) DeepCopyInto(out *TestObjectList) { 78 | *out = *in 79 | out.TypeMeta = in.TypeMeta 80 | in.ListMeta.DeepCopyInto(&out.ListMeta) 81 | if in.Items != nil { 82 | in, out := &in.Items, &out.Items 83 | *out = make([]TestObject, len(*in)) 84 | for i := range *in { 85 | (*in)[i].DeepCopyInto(&(*out)[i]) 86 | } 87 | } 88 | } 89 | 90 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestObjectList. 91 | func (in *TestObjectList) DeepCopy() *TestObjectList { 92 | if in == nil { 93 | return nil 94 | } 95 | out := new(TestObjectList) 96 | in.DeepCopyInto(out) 97 | return out 98 | } 99 | 100 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 101 | func (in *TestObjectList) DeepCopyObject() runtime.Object { 102 | if c := in.DeepCopy(); c != nil { 103 | return c 104 | } 105 | return nil 106 | } 107 | 108 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 109 | func (in *TestObjectSpec) DeepCopyInto(out *TestObjectSpec) { 110 | *out = *in 111 | } 112 | 113 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestObjectSpec. 114 | func (in *TestObjectSpec) DeepCopy() *TestObjectSpec { 115 | if in == nil { 116 | return nil 117 | } 118 | out := new(TestObjectSpec) 119 | in.DeepCopyInto(out) 120 | return out 121 | } 122 | 123 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 124 | func (in *TestObjectStatus) DeepCopyInto(out *TestObjectStatus) { 125 | *out = *in 126 | if in.Conditions != nil { 127 | in, out := &in.Conditions, &out.Conditions 128 | *out = make([]conditions.Condition, len(*in)) 129 | for i := range *in { 130 | (*in)[i].DeepCopyInto(&(*out)[i]) 131 | } 132 | } 133 | } 134 | 135 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestObjectStatus. 136 | func (in *TestObjectStatus) DeepCopy() *TestObjectStatus { 137 | if in == nil { 138 | return nil 139 | } 140 | out := new(TestObjectStatus) 141 | in.DeepCopyInto(out) 142 | return out 143 | } 144 | -------------------------------------------------------------------------------- /conditions/conditions.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package conditions 15 | 16 | import ( 17 | "time" 18 | 19 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 20 | ) 21 | 22 | // SetStatusCondition sets the corresponding condition in conditions to newCondition. 23 | // conditions must be non-nil. 24 | // 1. if the condition of the specified type already exists (all fields of the existing condition are updated to 25 | // newCondition, LastTransitionTime is set to now if the new status differs from the old status) 26 | // 2. if a condition of the specified type does not exist (LastTransitionTime is set to now() if unset, and newCondition is appended) 27 | func SetStatusCondition(conditions *[]Condition, newCondition Condition) { 28 | if conditions == nil { 29 | return 30 | } 31 | existingCondition := FindStatusCondition(*conditions, newCondition.Type) 32 | if existingCondition == nil { 33 | if newCondition.LastTransitionTime.IsZero() { 34 | newCondition.LastTransitionTime = metav1.NewTime(time.Now()) 35 | } 36 | *conditions = append(*conditions, newCondition) 37 | return 38 | } 39 | 40 | if existingCondition.Status != newCondition.Status { 41 | existingCondition.Status = newCondition.Status 42 | if !newCondition.LastTransitionTime.IsZero() { 43 | existingCondition.LastTransitionTime = newCondition.LastTransitionTime 44 | } else { 45 | existingCondition.LastTransitionTime = metav1.NewTime(time.Now()) 46 | } 47 | } 48 | 49 | existingCondition.Reason = newCondition.Reason 50 | existingCondition.Message = newCondition.Message 51 | } 52 | 53 | // RemoveStatusCondition removes the corresponding conditionType from conditions. 54 | // conditions must be non-nil. 55 | func RemoveStatusCondition(conditions *[]Condition, conditionType string) { 56 | if conditions == nil { 57 | return 58 | } 59 | newConditions := make([]Condition, 0, len(*conditions)-1) 60 | for _, condition := range *conditions { 61 | if condition.Type != conditionType { 62 | newConditions = append(newConditions, condition) 63 | } 64 | } 65 | 66 | *conditions = newConditions 67 | } 68 | 69 | // FindStatusCondition finds the conditionType in conditions. 70 | func FindStatusCondition(conditions []Condition, conditionType string) *Condition { 71 | for i := range conditions { 72 | if conditions[i].Type == conditionType { 73 | return &conditions[i] 74 | } 75 | } 76 | 77 | return nil 78 | } 79 | 80 | // IsStatusConditionTrue returns true when the conditionType is present and set to `ConditionTrue` 81 | func IsStatusConditionTrue(conditions []Condition, conditionType string) bool { 82 | return IsStatusConditionPresentAndEqual(conditions, conditionType, metav1.ConditionTrue) 83 | } 84 | 85 | // IsStatusConditionFalse returns true when the conditionType is present and set to `ConditionFalse` 86 | func IsStatusConditionFalse(conditions []Condition, conditionType string) bool { 87 | return IsStatusConditionPresentAndEqual(conditions, conditionType, metav1.ConditionFalse) 88 | } 89 | 90 | // IsStatusConditionPresentAndEqual returns true when conditionType is present and equal to status. 91 | func IsStatusConditionPresentAndEqual(conditions []Condition, conditionType string, status metav1.ConditionStatus) bool { 92 | for _, condition := range conditions { 93 | if condition.Type == conditionType { 94 | return condition.Status == status 95 | } 96 | } 97 | return false 98 | } 99 | -------------------------------------------------------------------------------- /conditions/deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | /* 5 | Copyright The Kubernetes Authors. 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by deepcopy-gen. DO NOT EDIT. 18 | 19 | package conditions 20 | 21 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 22 | func (in *Condition) DeepCopyInto(out *Condition) { 23 | *out = *in 24 | in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) 25 | return 26 | } 27 | 28 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. 29 | func (in *Condition) DeepCopy() *Condition { 30 | if in == nil { 31 | return nil 32 | } 33 | out := new(Condition) 34 | in.DeepCopyInto(out) 35 | return out 36 | } 37 | -------------------------------------------------------------------------------- /conditions/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 The Kubernetes Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package conditions 15 | 16 | import ( 17 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 | ) 19 | 20 | // Condition contains details for one aspect of the current state of this API Resource. 21 | // --- 22 | // This struct is intended for direct use as an array at the field path .status.conditions. For example, 23 | // 24 | // type FooStatus struct{ 25 | // // Represents the observations of a foo's current state. 26 | // // Known .status.conditions.type are: "Available", "Progressing", and "Degraded" 27 | // // +patchMergeKey=type 28 | // // +patchStrategy=merge 29 | // // +listType=map 30 | // // +listMapKey=type 31 | // Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` 32 | // 33 | // // other fields 34 | // } 35 | type Condition struct { 36 | // type of condition in CamelCase or in foo.example.com/CamelCase. 37 | // --- 38 | // Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be 39 | // useful (see .node.status.conditions), the ability to deconflict is important. 40 | // The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) 41 | // +required 42 | // +kubebuilder:validation:Required 43 | // +kubebuilder:validation:Pattern=`^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$` 44 | // +kubebuilder:validation:MaxLength=316 45 | Type string `json:"type" protobuf:"bytes,1,opt,name=type"` 46 | // status of the condition, one of True, False, Unknown. 47 | // +required 48 | // +kubebuilder:validation:Required 49 | // +kubebuilder:validation:Enum=True;False;Unknown 50 | Status metav1.ConditionStatus `json:"status" protobuf:"bytes,2,opt,name=status"` 51 | // observedGeneration represents the .metadata.generation that the condition was set based upon. 52 | // For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date 53 | // with respect to the current state of the instance. 54 | // +optional 55 | // +kubebuilder:validation:Minimum=0 56 | ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,3,opt,name=observedGeneration"` 57 | // lastTransitionTime is the last time the condition transitioned from one status to another. 58 | // This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. 59 | // +required 60 | // +kubebuilder:validation:Required 61 | // +kubebuilder:validation:Type=string 62 | // +kubebuilder:validation:Format=date-time 63 | LastTransitionTime metav1.Time `json:"lastTransitionTime" protobuf:"bytes,4,opt,name=lastTransitionTime"` 64 | // reason contains a programmatic identifier indicating the reason for the condition's last transition. 65 | // Producers of specific condition types may define expected values and meanings for this field, 66 | // and whether the values are considered a guaranteed API. 67 | // The value should be a CamelCase string. 68 | // This field may not be empty. 69 | // +required 70 | // +kubebuilder:validation:Required 71 | // +kubebuilder:validation:MaxLength=1024 72 | // +kubebuilder:validation:MinLength=1 73 | // +kubebuilder:validation:Pattern=`^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$` 74 | Reason string `json:"reason" protobuf:"bytes,5,opt,name=reason"` 75 | // message is a human readable message indicating details about the transition. 76 | // This may be an empty string. 77 | // +required 78 | // +kubebuilder:validation:Required 79 | // +kubebuilder:validation:MaxLength=32768 80 | Message string `json:"message" protobuf:"bytes,6,opt,name=message"` 81 | } 82 | -------------------------------------------------------------------------------- /core/components.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Noah Kantrowitz 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package core 18 | 19 | import ( 20 | "time" 21 | 22 | ctrl "sigs.k8s.io/controller-runtime" 23 | ) 24 | 25 | type Component interface { 26 | Reconcile(*Context) (Result, error) 27 | } 28 | 29 | type InitializerComponent interface { 30 | Setup(*Context, *ctrl.Builder) error 31 | } 32 | 33 | type FinalizerComponent interface { 34 | Finalize(*Context) (Result, bool, error) 35 | } 36 | 37 | type ReadyConditionComponent interface { 38 | GetReadyCondition() string 39 | } 40 | 41 | type Result struct { 42 | Requeue bool 43 | RequeueAfter time.Duration 44 | SkipRemaining bool 45 | } 46 | -------------------------------------------------------------------------------- /core/conditions.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Noah Kantrowitz 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package core 18 | 19 | import ( 20 | "fmt" 21 | "reflect" 22 | "strings" 23 | 24 | "github.com/pkg/errors" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | 27 | "github.com/coderanger/controller-utils/conditions" 28 | "sigs.k8s.io/controller-runtime/pkg/client" 29 | ) 30 | 31 | type ConditionsObject interface { 32 | GetConditions() *[]conditions.Condition 33 | } 34 | 35 | func GetConditionsFor(obj client.Object) (*[]conditions.Condition, error) { 36 | // Try the simple and correct way. 37 | condObj, ok := obj.(ConditionsObject) 38 | if ok { 39 | return condObj.GetConditions(), nil 40 | } 41 | 42 | // Supply a dynamic fallback until I can get some code generation in place. 43 | // Yes, I know this code is awful. 44 | statusVal := reflect.ValueOf(obj).FieldByName("Status") 45 | if statusVal.IsValid() { 46 | conditionsVal := statusVal.FieldByName("Conditions") 47 | if conditionsVal.IsValid() { 48 | maybeConditions := conditionsVal.Addr().Interface() 49 | conditions, ok := maybeConditions.(*[]conditions.Condition) 50 | if ok { 51 | return conditions, nil 52 | } 53 | } 54 | } 55 | 56 | return nil, errors.New("unable to get conditions") 57 | } 58 | 59 | type conditionsHelper struct { 60 | obj client.Object 61 | pendingConditions map[string]*conditions.Condition 62 | } 63 | 64 | func NewConditionsHelper(obj client.Object) *conditionsHelper { 65 | return &conditionsHelper{ 66 | obj: obj, 67 | pendingConditions: map[string]*conditions.Condition{}, 68 | } 69 | } 70 | 71 | func (h *conditionsHelper) Flush() error { 72 | conds, err := GetConditionsFor(h.obj) 73 | if err != nil { 74 | return errors.Wrap(err, "error getting status conditions") 75 | } 76 | // Apply all pending conditions. 77 | for _, cond := range h.pendingConditions { 78 | conditions.SetStatusCondition(conds, *cond) 79 | } 80 | // Zero out the pending map. 81 | h.pendingConditions = map[string]*conditions.Condition{} 82 | return nil 83 | } 84 | 85 | func (h *conditionsHelper) SetCondition(cond *conditions.Condition) { 86 | if cond.ObservedGeneration == 0 { 87 | cond.ObservedGeneration = h.obj.GetGeneration() 88 | } 89 | h.pendingConditions[cond.Type] = cond 90 | } 91 | 92 | func (h *conditionsHelper) Set(conditionType string, status metav1.ConditionStatus, reason string, message ...string) { 93 | h.SetCondition(&conditions.Condition{ 94 | Type: conditionType, 95 | Status: status, 96 | Reason: reason, 97 | Message: strings.Join(message, ""), 98 | }) 99 | } 100 | 101 | func (h *conditionsHelper) Setf(conditionType string, status metav1.ConditionStatus, reason string, message string, args ...interface{}) { 102 | h.Set(conditionType, status, reason, fmt.Sprintf(message, args...)) 103 | } 104 | 105 | func (h *conditionsHelper) SetTrue(conditionType string, reason string, message ...string) { 106 | h.Set(conditionType, metav1.ConditionTrue, reason, message...) 107 | } 108 | 109 | func (h *conditionsHelper) SetfTrue(conditionType string, reason string, message string, args ...interface{}) { 110 | h.Setf(conditionType, metav1.ConditionTrue, reason, message, args...) 111 | } 112 | 113 | func (h *conditionsHelper) SetFalse(conditionType string, reason string, message ...string) { 114 | h.Set(conditionType, metav1.ConditionFalse, reason, message...) 115 | } 116 | 117 | func (h *conditionsHelper) SetfFalse(conditionType string, reason string, message string, args ...interface{}) { 118 | h.Setf(conditionType, metav1.ConditionFalse, reason, message, args...) 119 | } 120 | 121 | func (h *conditionsHelper) SetUnknown(conditionType string, reason string, message ...string) { 122 | h.Set(conditionType, metav1.ConditionUnknown, reason, message...) 123 | } 124 | 125 | func (h *conditionsHelper) SetfUnknown(conditionType string, reason string, message string, args ...interface{}) { 126 | h.Setf(conditionType, metav1.ConditionUnknown, reason, message, args...) 127 | } 128 | -------------------------------------------------------------------------------- /core/context.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Noah Kantrowitz 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package core 18 | 19 | import ( 20 | "context" 21 | "net/http" 22 | 23 | "github.com/go-logr/logr" 24 | "github.com/pkg/errors" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | "k8s.io/client-go/tools/record" 27 | ctrl "sigs.k8s.io/controller-runtime" 28 | "sigs.k8s.io/controller-runtime/pkg/client" 29 | ) 30 | 31 | type ContextData map[string]interface{} 32 | 33 | type Context struct { 34 | context.Context 35 | Object client.Object 36 | Client client.Client 37 | UncachedClient client.Client 38 | Log logr.Logger 39 | // Pending result at the end of things. 40 | result ctrl.Result 41 | // Errors from components. 42 | errors []error 43 | // Templates filesystem, mostly used through helpers but accessible directly too. 44 | Templates http.FileSystem 45 | // Name to use as the field manager with Apply. 46 | FieldManager string 47 | // API Scheme for use with other helpers. 48 | Scheme *runtime.Scheme 49 | // Arbitrary data used to communicate between components during a reconcile. 50 | Data ContextData 51 | // Event recorder to emit event objects. 52 | Events record.EventRecorder 53 | // Helper for setting status conditions. 54 | Conditions *conditionsHelper 55 | } 56 | 57 | func (c *Context) mergeResult(name string, componentResult Result, err error) { 58 | condErr := c.Conditions.Flush() 59 | if condErr != nil { 60 | c.errors = append(c.errors, errors.Wrapf(err, "error in %s component condition flush", name)) 61 | } 62 | if err != nil { 63 | c.errors = append(c.errors, errors.Wrapf(err, "error in %s component reconcile", name)) 64 | } 65 | if componentResult.Requeue { 66 | c.result.Requeue = true 67 | } 68 | if componentResult.RequeueAfter != 0 && (c.result.RequeueAfter == 0 || c.result.RequeueAfter > componentResult.RequeueAfter) { 69 | c.result.RequeueAfter = componentResult.RequeueAfter 70 | } 71 | } 72 | 73 | func (d ContextData) GetString(key string) (string, bool) { 74 | val, ok := d[key] 75 | return val.(string), ok 76 | } 77 | 78 | // TODO Other type accessors for ContextData. 79 | -------------------------------------------------------------------------------- /core/reconciler.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Noah Kantrowitz 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package core 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "net/http" 23 | "strings" 24 | 25 | "github.com/go-logr/logr" 26 | "github.com/pkg/errors" 27 | kerrors "k8s.io/apimachinery/pkg/api/errors" 28 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 | "k8s.io/client-go/tools/record" 30 | ctrl "sigs.k8s.io/controller-runtime" 31 | "sigs.k8s.io/controller-runtime/pkg/builder" 32 | "sigs.k8s.io/controller-runtime/pkg/client" 33 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 34 | "sigs.k8s.io/controller-runtime/pkg/controller" 35 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 36 | "sigs.k8s.io/controller-runtime/pkg/handler" 37 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 38 | "sigs.k8s.io/controller-runtime/pkg/source" 39 | ) 40 | 41 | // Supporting mocking out functions for testing 42 | var getGvk = apiutil.GVKForObject 43 | 44 | // Avoid an import loop. Sighs in Go. 45 | var NewRandomSecretComponent func(string, ...string) Component 46 | var NewReadyStatusComponent func(...string) Component 47 | var NewTemplateComponent func(string, string) Component 48 | 49 | type Reconciler struct { 50 | name string 51 | mgr ctrl.Manager 52 | controllerBuilder *ctrl.Builder 53 | controller controller.Controller 54 | apiType client.Object 55 | components []*reconcilerComponent 56 | log logr.Logger 57 | client client.Client 58 | uncachedClient client.Client 59 | templates http.FileSystem 60 | events record.EventRecorder 61 | webhook bool 62 | finalizerBaseName string 63 | } 64 | 65 | // Concrete component instance. 66 | type reconcilerComponent struct { 67 | name string 68 | comp Component 69 | // Same component as comp but as a finalizer if possible, otherwise nil. 70 | finalizer FinalizerComponent 71 | finalizerName string 72 | // Tracking data for status conditions. 73 | readyCondition string 74 | errorConditionStatus metav1.ConditionStatus 75 | } 76 | 77 | func NewReconciler(mgr ctrl.Manager) *Reconciler { 78 | rawClient, err := client.New(mgr.GetConfig(), client.Options{Scheme: mgr.GetScheme()}) 79 | if err != nil { 80 | panic(err) 81 | } 82 | return &Reconciler{ 83 | mgr: mgr, 84 | controllerBuilder: builder.ControllerManagedBy(mgr), 85 | components: []*reconcilerComponent{}, 86 | client: mgr.GetClient(), 87 | uncachedClient: rawClient, 88 | } 89 | } 90 | 91 | func (r *Reconciler) For(apiType client.Object, opts ...builder.ForOption) *Reconciler { 92 | r.apiType = apiType 93 | r.controllerBuilder = r.controllerBuilder.For(apiType, opts...) 94 | return r 95 | } 96 | 97 | func (r *Reconciler) Watches(src source.Source, eventhandler handler.EventHandler, opts ...builder.WatchesOption) *Reconciler { 98 | r.controllerBuilder = r.controllerBuilder.Watches(src, eventhandler, opts...) 99 | return r 100 | } 101 | 102 | func (r *Reconciler) Templates(t http.FileSystem) *Reconciler { 103 | r.templates = t 104 | return r 105 | } 106 | 107 | func (r *Reconciler) Webhook() *Reconciler { 108 | r.webhook = true 109 | return r 110 | } 111 | 112 | func (r *Reconciler) Component(name string, comp Component) *Reconciler { 113 | rc := &reconcilerComponent{name: name, comp: comp} 114 | finalizer, ok := comp.(FinalizerComponent) 115 | if ok { 116 | rc.finalizer = finalizer 117 | } 118 | readyCond, ok := comp.(ReadyConditionComponent) 119 | if ok { 120 | rc.readyCondition = readyCond.GetReadyCondition() 121 | rc.errorConditionStatus = metav1.ConditionFalse 122 | // If the first character is !, trim it off and invert the error status. 123 | if rc.readyCondition != "" && rc.readyCondition[0] == '!' { 124 | rc.readyCondition = rc.readyCondition[1:] 125 | rc.errorConditionStatus = metav1.ConditionTrue 126 | } 127 | } 128 | r.components = append(r.components, rc) 129 | return r 130 | } 131 | 132 | func (r *Reconciler) TemplateComponent(template string, conditionType string) *Reconciler { 133 | name := template[:strings.LastIndex(template, ".")] 134 | return r.Component(name, NewTemplateComponent(template, conditionType)) 135 | } 136 | 137 | func (r *Reconciler) RandomSecretComponent(keys ...string) *Reconciler { 138 | // TODO This is super awkward. Maybe just provisionally set r.name from For()? 139 | controllerName, err := r.getControllerName() 140 | if err != nil { 141 | panic(err) 142 | } 143 | nameTemplate := fmt.Sprintf("%%s-%s", controllerName) 144 | return r.Component("randomSecret", NewRandomSecretComponent(nameTemplate, keys...)) 145 | } 146 | 147 | func (r *Reconciler) ReadyStatusComponent(keys ...string) *Reconciler { 148 | return r.Component("readyStatus", NewReadyStatusComponent(keys...)) 149 | } 150 | 151 | func (r *Reconciler) Complete() error { 152 | _, err := r.Build() 153 | return err 154 | } 155 | 156 | func (r *Reconciler) getControllerName() (string, error) { 157 | if r.name != "" { 158 | return r.name, nil 159 | } 160 | gvk, err := getGvk(r.apiType, r.mgr.GetScheme()) 161 | if err != nil { 162 | return "", err 163 | } 164 | return strings.ToLower(gvk.Kind), nil 165 | } 166 | 167 | func (r *Reconciler) Build() (controller.Controller, error) { 168 | name, err := r.getControllerName() 169 | if err != nil { 170 | return nil, errors.Wrap(err, "error computing controller name") 171 | } 172 | r.name = name 173 | r.log = ctrl.Log.WithName("controllers").WithName(name) 174 | 175 | // Work out a default finalizer base name. 176 | if r.finalizerBaseName == "" { 177 | gvk, err := apiutil.GVKForObject(r.apiType, r.mgr.GetScheme()) 178 | if err != nil { 179 | return nil, errors.Wrapf(err, "error getting GVK for object %#v", r.apiType) 180 | } 181 | r.finalizerBaseName = fmt.Sprintf("%s.%s/", name, gvk.Group) 182 | } 183 | 184 | // Check if we have more than component with the same name. 185 | compMap := map[string]Component{} 186 | for _, rc := range r.components { 187 | first, ok := compMap[rc.name] 188 | if ok { 189 | return nil, errors.Errorf("found duplicate component using name %s: %#v %#v", rc.name, first, rc.comp) 190 | } 191 | compMap[rc.name] = rc.comp 192 | } 193 | 194 | setupCtx := &Context{ 195 | Context: context.Background(), 196 | Client: r.client, 197 | UncachedClient: r.uncachedClient, 198 | Templates: r.templates, 199 | Scheme: r.mgr.GetScheme(), 200 | Object: r.apiType.DeepCopyObject().(client.Object), 201 | } 202 | // Provide some bare minimum data 203 | setupObj := setupCtx.Object.(metav1.Object) 204 | setupObj.SetName("setup") 205 | setupObj.SetNamespace("setup") 206 | log := r.log.WithName("components") 207 | for _, rc := range r.components { 208 | rc.finalizerName = r.finalizerBaseName + rc.name 209 | setupComp, ok := rc.comp.(InitializerComponent) 210 | if !ok { 211 | continue 212 | } 213 | setupCtx.Log = log.WithName(rc.name) 214 | setupCtx.FieldManager = fmt.Sprintf("%s/%s", r.name, rc.name) 215 | err := setupComp.Setup(setupCtx, r.controllerBuilder) 216 | if err != nil { 217 | return nil, errors.Wrapf(err, "error initializing component %s in controller %s", rc.name, r.name) 218 | } 219 | } 220 | controller, err := r.controllerBuilder.Build(r) 221 | if err != nil { 222 | return nil, errors.Wrapf(err, "error building controller %s", r.name) 223 | } 224 | r.controller = controller 225 | r.events = r.mgr.GetEventRecorderFor(r.name + "-controller") 226 | // If requested, set up a webhook runable too. 227 | if r.webhook { 228 | err := ctrl.NewWebhookManagedBy(r.mgr).For(r.apiType).Complete() 229 | if err != nil { 230 | return nil, errors.Wrap(err, "error initializing webhook") 231 | } 232 | } 233 | return controller, nil 234 | } 235 | 236 | func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 237 | log := r.log.WithValues("object", req) 238 | log.Info("Starting reconcile") 239 | 240 | recCtx := &Context{ 241 | Context: ctx, 242 | Client: r.client, 243 | UncachedClient: r.uncachedClient, 244 | Templates: r.templates, 245 | Scheme: r.mgr.GetScheme(), 246 | Events: r.events, 247 | Data: ContextData{}, 248 | } 249 | 250 | obj := r.apiType.DeepCopyObject().(client.Object) 251 | err := r.client.Get(recCtx, req.NamespacedName, obj) 252 | if err != nil { 253 | if kerrors.IsNotFound(err) { 254 | // Object not found, likely already deleted, just silenty bail. 255 | log.Info("Aborting reconcile, object already deleted") 256 | return reconcile.Result{}, nil 257 | } 258 | return reconcile.Result{Requeue: true}, errors.Wrap(err, "error getting reconcile object") 259 | } 260 | recCtx.Object = obj.(client.Object) 261 | 262 | recCtx.Conditions = NewConditionsHelper(recCtx.Object) 263 | cleanObj := obj.DeepCopyObject().(client.Object) 264 | 265 | // Check for annotation that blocks reconciles, exit early if found. 266 | annotations := recCtx.Object.GetAnnotations() 267 | reconcileBlocked, ok := annotations["controller-utils/skip-reconcile"] 268 | if ok && reconcileBlocked == "true" { 269 | log.Info("Skipping reconcile due to annotation") 270 | return reconcile.Result{}, nil 271 | } 272 | 273 | // Reconcile the components. 274 | compLog := log.WithName("components") 275 | for _, rc := range r.components { 276 | // Create the per-component logger. 277 | recCtx.Log = compLog.WithName(rc.name) 278 | recCtx.FieldManager = fmt.Sprintf("%s/%s", r.name, rc.name) 279 | isAlive := recCtx.Object.GetDeletionTimestamp() == nil 280 | if rc.readyCondition != "" { 281 | recCtx.Conditions.SetUnknown(rc.readyCondition, "Unknown") 282 | } 283 | var res Result 284 | if isAlive { 285 | log.V(1).Info("Reconciling component", "component", rc.name) 286 | res, err = rc.comp.Reconcile(recCtx) 287 | if rc.finalizer != nil { 288 | controllerutil.AddFinalizer(recCtx.Object, rc.finalizerName) 289 | } 290 | } else if rc.finalizer != nil && controllerutil.ContainsFinalizer(recCtx.Object, rc.finalizerName) { 291 | log.V(1).Info("Finalizing component", "component", rc.name) 292 | var done bool 293 | res, done, err = rc.finalizer.Finalize(recCtx) 294 | if done { 295 | controllerutil.RemoveFinalizer(recCtx.Object, rc.finalizerName) 296 | } 297 | } 298 | if err != nil && rc.readyCondition != "" { 299 | // Mark the status condition for this component as bad. 300 | recCtx.Conditions.Set(rc.readyCondition, rc.errorConditionStatus, "Error", err.Error()) 301 | } 302 | recCtx.mergeResult(rc.name, res, err) 303 | if err != nil { 304 | log.Error(err, "error in component reconcile", "component", rc.name) 305 | } 306 | if res.SkipRemaining { 307 | // Abort reconcile to skip remaining components. 308 | log.V(1).Info("Skipping remaining components") 309 | break 310 | } 311 | } 312 | 313 | // Check if we need to patch metadata, only looking at labels, annotations, and finalizers. 314 | currentMeta := r.apiType.DeepCopyObject().(client.Object) 315 | currentMeta.SetName(recCtx.Object.GetName()) 316 | currentMeta.SetNamespace(recCtx.Object.GetNamespace()) 317 | currentMeta.SetLabels(recCtx.Object.GetLabels()) 318 | currentMeta.SetAnnotations(recCtx.Object.GetAnnotations()) 319 | currentMeta.SetFinalizers(recCtx.Object.GetFinalizers()) 320 | cleanMeta := r.apiType.DeepCopyObject().(client.Object) 321 | cleanMeta.SetName(cleanObj.GetName()) 322 | cleanMeta.SetNamespace(cleanObj.GetNamespace()) 323 | cleanMeta.SetLabels(cleanObj.GetLabels()) 324 | cleanMeta.SetAnnotations(cleanObj.GetAnnotations()) 325 | cleanMeta.SetFinalizers(cleanObj.GetFinalizers()) 326 | err = r.client.Patch(recCtx, currentMeta, client.MergeFrom(cleanMeta), &client.PatchOptions{FieldManager: r.name}) 327 | if err != nil && !kerrors.IsNotFound(err) { 328 | // If it was a NotFound error, the object was probably already deleted so just ignore the error and return the existing result. 329 | return recCtx.result, errors.Wrap(err, "error patching metadata") 330 | } 331 | 332 | // Save the object status. 333 | err = r.client.Status().Patch(recCtx, recCtx.Object, client.MergeFrom(cleanObj), &client.PatchOptions{FieldManager: r.name}) 334 | 335 | if err != nil && !kerrors.IsNotFound(err) { 336 | // If it was a NotFound error, the object was probably already deleted so just ignore the error and return the existing result. 337 | return recCtx.result, errors.Wrap(err, "error patching status") 338 | } 339 | 340 | // Build up the final error to be logged. 341 | err = nil 342 | if len(recCtx.errors) == 1 { 343 | err = recCtx.errors[0] 344 | } else if len(recCtx.errors) > 1 { 345 | msg := strings.Builder{} 346 | msg.WriteString("Multiple errors:\n") 347 | for _, e := range recCtx.errors { 348 | msg.WriteString(" ") 349 | msg.WriteString(e.Error()) 350 | msg.WriteString("\n") 351 | } 352 | err = errors.New(msg.String()) 353 | } 354 | 355 | return recCtx.result, err 356 | } 357 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/coderanger/controller-utils 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/Masterminds/sprig v2.22.0+incompatible 7 | github.com/go-logr/logr v1.2.3 8 | github.com/onsi/ginkgo v1.16.5 9 | github.com/onsi/gomega v1.19.0 10 | github.com/pkg/errors v0.9.1 11 | github.com/prometheus/client_golang v1.12.2 12 | github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 13 | gopkg.in/yaml.v2 v2.4.0 14 | k8s.io/api v0.25.0 15 | k8s.io/apiextensions-apiserver v0.25.0 16 | k8s.io/apimachinery v0.25.0 17 | k8s.io/client-go v0.25.0 18 | sigs.k8s.io/controller-runtime v0.13.0 19 | sigs.k8s.io/yaml v1.3.0 20 | ) 21 | 22 | require ( 23 | github.com/Masterminds/goutils v1.1.0 // indirect 24 | github.com/Masterminds/semver v1.5.0 // indirect 25 | github.com/PuerkitoBio/purell v1.1.1 // indirect 26 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 27 | github.com/beorn7/perks v1.0.1 // indirect 28 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 29 | github.com/davecgh/go-spew v1.1.1 // indirect 30 | github.com/emicklei/go-restful/v3 v3.8.0 // indirect 31 | github.com/evanphx/json-patch v4.12.0+incompatible // indirect 32 | github.com/evanphx/json-patch/v5 v5.6.0 // indirect 33 | github.com/fsnotify/fsnotify v1.5.4 // indirect 34 | github.com/go-logr/zapr v1.2.3 // indirect 35 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 36 | github.com/go-openapi/jsonreference v0.19.5 // indirect 37 | github.com/go-openapi/swag v0.19.14 // indirect 38 | github.com/gogo/protobuf v1.3.2 // indirect 39 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 40 | github.com/golang/protobuf v1.5.2 // indirect 41 | github.com/google/gnostic v0.5.7-v3refs // indirect 42 | github.com/google/go-cmp v0.5.8 // indirect 43 | github.com/google/gofuzz v1.1.0 // indirect 44 | github.com/google/uuid v1.1.2 // indirect 45 | github.com/huandu/xstrings v1.3.2 // indirect 46 | github.com/imdario/mergo v0.3.12 // indirect 47 | github.com/josharian/intern v1.0.0 // indirect 48 | github.com/json-iterator/go v1.1.12 // indirect 49 | github.com/mailru/easyjson v0.7.6 // indirect 50 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect 51 | github.com/mitchellh/copystructure v1.0.0 // indirect 52 | github.com/mitchellh/reflectwalk v1.0.0 // indirect 53 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 54 | github.com/modern-go/reflect2 v1.0.2 // indirect 55 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 56 | github.com/nxadm/tail v1.4.8 // indirect 57 | github.com/prometheus/client_model v0.2.0 // indirect 58 | github.com/prometheus/common v0.32.1 // indirect 59 | github.com/prometheus/procfs v0.7.3 // indirect 60 | github.com/spf13/pflag v1.0.5 // indirect 61 | go.uber.org/atomic v1.7.0 // indirect 62 | go.uber.org/multierr v1.6.0 // indirect 63 | go.uber.org/zap v1.21.0 // indirect 64 | golang.org/x/crypto v0.11.0 // indirect 65 | golang.org/x/net v0.10.0 // indirect 66 | golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect 67 | golang.org/x/sys v0.10.0 // indirect 68 | golang.org/x/term v0.10.0 // indirect 69 | golang.org/x/text v0.11.0 // indirect 70 | golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect 71 | gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect 72 | google.golang.org/appengine v1.6.7 // indirect 73 | google.golang.org/protobuf v1.28.0 // indirect 74 | gopkg.in/inf.v0 v0.9.1 // indirect 75 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 76 | gopkg.in/yaml.v3 v3.0.1 // indirect 77 | k8s.io/component-base v0.25.0 // indirect 78 | k8s.io/klog/v2 v2.70.1 // indirect 79 | k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect 80 | k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect 81 | sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect 82 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 83 | ) 84 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 9 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 12 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 13 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 14 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 15 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 16 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 17 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 18 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 19 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 20 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 21 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 22 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 23 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 24 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 25 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 26 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 27 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 28 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 29 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 30 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 31 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 32 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 33 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 34 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 35 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 36 | github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= 37 | github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 38 | github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= 39 | github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 40 | github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= 41 | github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= 42 | github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= 43 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 44 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= 45 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 46 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 47 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 48 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 49 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 50 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 51 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= 52 | github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 53 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 54 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 55 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 56 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 57 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 58 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 59 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 60 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 61 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 62 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 63 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 64 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 65 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 66 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 67 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 68 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 69 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 70 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= 71 | github.com/emicklei/go-restful/v3 v3.8.0 h1:eCZ8ulSerjdAiaNpF7GxXIE7ZCMo1moN1qX+S609eVw= 72 | github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 73 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 74 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 75 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 76 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 77 | github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= 78 | github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= 79 | github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 80 | github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= 81 | github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= 82 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 83 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 84 | github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= 85 | github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= 86 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 87 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 88 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 89 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 90 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 91 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 92 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 93 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 94 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 95 | github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= 96 | github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 97 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 98 | github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= 99 | github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 100 | github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= 101 | github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4= 102 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 103 | github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= 104 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 105 | github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= 106 | github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= 107 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 108 | github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= 109 | github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= 110 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 111 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 112 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 113 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 114 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 115 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 116 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 117 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 118 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 119 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 120 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 121 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 122 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 123 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 124 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 125 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 126 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 127 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 128 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 129 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 130 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 131 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 132 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 133 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 134 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 135 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 136 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 137 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 138 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 139 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 140 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 141 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 142 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 143 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 144 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 145 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 146 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 147 | github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= 148 | github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= 149 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 150 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 151 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 152 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 153 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 154 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 155 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 156 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 157 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 158 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 159 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 160 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 161 | github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= 162 | github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 163 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 164 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 165 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 166 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 167 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 168 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 169 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 170 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 171 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 172 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 173 | github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= 174 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 175 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 176 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 177 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 178 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 179 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 180 | github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= 181 | github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 182 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 183 | github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= 184 | github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 185 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 186 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 187 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 188 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 189 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 190 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 191 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 192 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 193 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 194 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 195 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 196 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 197 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 198 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 199 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 200 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 201 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 202 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 203 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 204 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 205 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 206 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 207 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 208 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 209 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 210 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 211 | github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= 212 | github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 213 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 214 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= 215 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 216 | github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= 217 | github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= 218 | github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= 219 | github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 220 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 221 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 222 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 223 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 224 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 225 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 226 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 227 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 228 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 229 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 230 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 231 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 232 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 233 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 234 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 235 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 236 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 237 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 238 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 239 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 240 | github.com/onsi/ginkgo/v2 v2.1.4 h1:GNapqRSid3zijZ9H77KrgVG4/8KqiyRsxcSxe+7ApXY= 241 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 242 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 243 | github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= 244 | github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= 245 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 246 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 247 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 248 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 249 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 250 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 251 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 252 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 253 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 254 | github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= 255 | github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34= 256 | github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= 257 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 258 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 259 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 260 | github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= 261 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 262 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 263 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 264 | github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= 265 | github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= 266 | github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= 267 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 268 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 269 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 270 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 271 | github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= 272 | github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 273 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 274 | github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 h1:bUGsEnyNbVPw06Bs80sCeARAlK8lhwqGyi6UT8ymuGk= 275 | github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= 276 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 277 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 278 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 279 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= 280 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 281 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 282 | github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= 283 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 284 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 285 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 286 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 287 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 288 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 289 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 290 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 291 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 292 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 293 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 294 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 295 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 296 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 297 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 298 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 299 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 300 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 301 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 302 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 303 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 304 | go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= 305 | go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 306 | go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= 307 | go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= 308 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 309 | go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= 310 | go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= 311 | go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= 312 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 313 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 314 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 315 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 316 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 317 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 318 | golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= 319 | golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= 320 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 321 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 322 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 323 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 324 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 325 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 326 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 327 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 328 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 329 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 330 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 331 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 332 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 333 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 334 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 335 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 336 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 337 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 338 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 339 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 340 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 341 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 342 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 343 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 344 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 345 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 346 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 347 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 348 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 349 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 350 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 351 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 352 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 353 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 354 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 355 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 356 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 357 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 358 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 359 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 360 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 361 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 362 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 363 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 364 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 365 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 366 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 367 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 368 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 369 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 370 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 371 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 372 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 373 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 374 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 375 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 376 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 377 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 378 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 379 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 380 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 381 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 382 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 383 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 384 | golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 385 | golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= 386 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 387 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 388 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 389 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 390 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 391 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 392 | golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 393 | golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg= 394 | golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 395 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 396 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 397 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 398 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 399 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 400 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 401 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 402 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 403 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 404 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 405 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 406 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 407 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 408 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 409 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 410 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 411 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 412 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 413 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 414 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 415 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 416 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 417 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 418 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 419 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 420 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 421 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 422 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 423 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 424 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 425 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 426 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 427 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 428 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 429 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 430 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 431 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 432 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 433 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 434 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 435 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 436 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 437 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 438 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 439 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 440 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 441 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 442 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 443 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 444 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 445 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 446 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 447 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 448 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 449 | golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 450 | golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 451 | golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= 452 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 453 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 454 | golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= 455 | golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= 456 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 457 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 458 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 459 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 460 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 461 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 462 | golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= 463 | golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 464 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 465 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 466 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 467 | golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U= 468 | golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 469 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 470 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 471 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 472 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 473 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 474 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 475 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 476 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 477 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 478 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 479 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 480 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 481 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 482 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 483 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 484 | golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 485 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 486 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 487 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 488 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 489 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 490 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 491 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 492 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 493 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 494 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 495 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 496 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 497 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 498 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 499 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 500 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 501 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 502 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 503 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 504 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 505 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 506 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 507 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 508 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 509 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 510 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 511 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 512 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 513 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 514 | golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= 515 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 516 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 517 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 518 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 519 | gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= 520 | gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= 521 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 522 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 523 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 524 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 525 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 526 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 527 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 528 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 529 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 530 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 531 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 532 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 533 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 534 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 535 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 536 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 537 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 538 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 539 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 540 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 541 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 542 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 543 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 544 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 545 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 546 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 547 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 548 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 549 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 550 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 551 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 552 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 553 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 554 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 555 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 556 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 557 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 558 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 559 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 560 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 561 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 562 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 563 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 564 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 565 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 566 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 567 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 568 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 569 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 570 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 571 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 572 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 573 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 574 | google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 575 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 576 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 577 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 578 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 579 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 580 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 581 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 582 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 583 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 584 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 585 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 586 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 587 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 588 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 589 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 590 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 591 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 592 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 593 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 594 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 595 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 596 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 597 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 598 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 599 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 600 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 601 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 602 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 603 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 604 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 605 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 606 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 607 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 608 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 609 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 610 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 611 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 612 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 613 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 614 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 615 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 616 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 617 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 618 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 619 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 620 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 621 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 622 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 623 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 624 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 625 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 626 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 627 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 628 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 629 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 630 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 631 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 632 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 633 | k8s.io/api v0.25.0 h1:H+Q4ma2U/ww0iGB78ijZx6DRByPz6/733jIuFpX70e0= 634 | k8s.io/api v0.25.0/go.mod h1:ttceV1GyV1i1rnmvzT3BST08N6nGt+dudGrquzVQWPk= 635 | k8s.io/apiextensions-apiserver v0.25.0 h1:CJ9zlyXAbq0FIW8CD7HHyozCMBpDSiH7EdrSTCZcZFY= 636 | k8s.io/apiextensions-apiserver v0.25.0/go.mod h1:3pAjZiN4zw7R8aZC5gR0y3/vCkGlAjCazcg1me8iB/E= 637 | k8s.io/apimachinery v0.25.0 h1:MlP0r6+3XbkUG2itd6vp3oxbtdQLQI94fD5gCS+gnoU= 638 | k8s.io/apimachinery v0.25.0/go.mod h1:qMx9eAk0sZQGsXGu86fab8tZdffHbwUfsvzqKn4mfB0= 639 | k8s.io/client-go v0.25.0 h1:CVWIaCETLMBNiTUta3d5nzRbXvY5Hy9Dpl+VvREpu5E= 640 | k8s.io/client-go v0.25.0/go.mod h1:lxykvypVfKilxhTklov0wz1FoaUZ8X4EwbhS6rpRfN8= 641 | k8s.io/component-base v0.25.0 h1:haVKlLkPCFZhkcqB6WCvpVxftrg6+FK5x1ZuaIDaQ5Y= 642 | k8s.io/component-base v0.25.0/go.mod h1:F2Sumv9CnbBlqrpdf7rKZTmmd2meJq0HizeyY/yAFxk= 643 | k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= 644 | k8s.io/klog/v2 v2.70.1 h1:7aaoSdahviPmR+XkS7FyxlkkXs6tHISSG03RxleQAVQ= 645 | k8s.io/klog/v2 v2.70.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= 646 | k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 h1:MQ8BAZPZlWk3S9K4a9NCkIFQtZShWqoha7snGixVgEA= 647 | k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1/go.mod h1:C/N6wCaBHeBHkHUesQOQy2/MZqGgMAFPqGsGQLdbZBU= 648 | k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed h1:jAne/RjBTyawwAy0utX5eqigAwz/lQhTmy+Hr/Cpue4= 649 | k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= 650 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 651 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 652 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 653 | sigs.k8s.io/controller-runtime v0.13.0 h1:iqa5RNciy7ADWnIc8QxCbOX5FEKVR3uxVxKHRMc2WIQ= 654 | sigs.k8s.io/controller-runtime v0.13.0/go.mod h1:Zbz+el8Yg31jubvAEyglRZGdLAjplZl+PgtYNI6WNTI= 655 | sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= 656 | sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 657 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= 658 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= 659 | sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= 660 | sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= 661 | -------------------------------------------------------------------------------- /predicates/deep_equals.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Noah Kantrowitz 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package predicates 18 | 19 | import ( 20 | "reflect" 21 | 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | "sigs.k8s.io/controller-runtime/pkg/event" 24 | "sigs.k8s.io/controller-runtime/pkg/predicate" 25 | ) 26 | 27 | // Predicate that uses DeepEquals to work around https://github.com/kubernetes/kubernetes/issues/95460. 28 | type deepEqualsPredicate struct{} 29 | 30 | func DeepEquals() *deepEqualsPredicate { 31 | return &deepEqualsPredicate{} 32 | } 33 | 34 | var _ predicate.Predicate = &deepEqualsPredicate{} 35 | 36 | // Create returns true if the Create event should be processed 37 | func (_ *deepEqualsPredicate) Create(_ event.CreateEvent) bool { 38 | return true 39 | } 40 | 41 | // Delete returns true if the Delete event should be processed 42 | func (_ *deepEqualsPredicate) Delete(_ event.DeleteEvent) bool { 43 | return true 44 | } 45 | 46 | // Update returns true if the Update event should be processed 47 | func (_ *deepEqualsPredicate) Update(evt event.UpdateEvent) bool { 48 | cleanOld := evt.ObjectOld.DeepCopyObject().(metav1.Object) 49 | cleanNew := evt.ObjectNew.DeepCopyObject().(metav1.Object) 50 | cleanOld.SetGeneration(0) 51 | cleanNew.SetGeneration(0) 52 | cleanOld.SetResourceVersion("") 53 | cleanNew.SetResourceVersion("") 54 | cleanOld.SetManagedFields([]metav1.ManagedFieldsEntry{}) 55 | cleanNew.SetManagedFields([]metav1.ManagedFieldsEntry{}) 56 | return !reflect.DeepEqual(cleanNew, cleanOld) 57 | } 58 | 59 | // Generic returns true if the Generic event should be processed 60 | func (_ *deepEqualsPredicate) Generic(_ event.GenericEvent) bool { 61 | return true 62 | } 63 | -------------------------------------------------------------------------------- /predicates/secret_field.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Noah Kantrowitz 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package predicates 18 | 19 | import ( 20 | "bytes" 21 | "encoding/base64" 22 | 23 | corev1 "k8s.io/api/core/v1" 24 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | "sigs.k8s.io/controller-runtime/pkg/event" 27 | "sigs.k8s.io/controller-runtime/pkg/predicate" 28 | ) 29 | 30 | type secretFieldPredicate struct { 31 | keys []string 32 | } 33 | 34 | func SecretField(keys []string) *secretFieldPredicate { 35 | return &secretFieldPredicate{keys: keys} 36 | } 37 | 38 | var _ predicate.Predicate = &secretFieldPredicate{} 39 | 40 | // Create returns true if the Create event should be processed 41 | func (_ *secretFieldPredicate) Create(_ event.CreateEvent) bool { 42 | return true 43 | } 44 | 45 | // Delete returns true if the Delete event should be processed 46 | func (_ *secretFieldPredicate) Delete(_ event.DeleteEvent) bool { 47 | return true 48 | } 49 | 50 | // Update returns true if the Update event should be processed 51 | func (p *secretFieldPredicate) Update(evt event.UpdateEvent) bool { 52 | oldData, ok := p.secretData(evt.ObjectOld) 53 | if !ok { 54 | return true 55 | } 56 | newData, ok := p.secretData(evt.ObjectNew) 57 | if !ok { 58 | return true 59 | } 60 | for _, key := range p.keys { 61 | oldVal, oldOk := oldData[key] 62 | newVal, newOk := newData[key] 63 | if oldOk != newOk || !bytes.Equal(oldVal, newVal) { 64 | return true 65 | } 66 | } 67 | return false 68 | } 69 | 70 | // Generic returns true if the Generic event should be processed 71 | func (_ *secretFieldPredicate) Generic(_ event.GenericEvent) bool { 72 | return true 73 | } 74 | 75 | func (_ *secretFieldPredicate) secretData(obj runtime.Object) (map[string][]byte, bool) { 76 | secret, ok := obj.(*corev1.Secret) 77 | if ok { 78 | return secret.Data, true 79 | } 80 | unstructured, ok := obj.(*unstructured.Unstructured) 81 | if ok { 82 | gvk := obj.GetObjectKind().GroupVersionKind() 83 | if gvk.Group == "" && gvk.Kind == "Secret" { 84 | data, ok := unstructured.UnstructuredContent()["data"] 85 | if ok { 86 | // Because unstructured skips the base64 decode, we have to do that now. 87 | cleanData := map[string][]byte{} 88 | for k, v := range data.(map[string]interface{}) { 89 | cleanV, err := base64.StdEncoding.DecodeString(v.(string)) 90 | if err != nil { 91 | // kube-apiserver sent us corrupted data, fuck it. 92 | panic(err) 93 | } 94 | cleanData[k] = cleanV 95 | } 96 | return cleanData, true 97 | } 98 | } 99 | } 100 | return nil, false 101 | } 102 | -------------------------------------------------------------------------------- /predicates/update_debug.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Noah Kantrowitz 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package predicates 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | 23 | "sigs.k8s.io/controller-runtime/pkg/client" 24 | "sigs.k8s.io/controller-runtime/pkg/event" 25 | logf "sigs.k8s.io/controller-runtime/pkg/log" 26 | "sigs.k8s.io/controller-runtime/pkg/predicate" 27 | ) 28 | 29 | var updateDebugLog = logf.Log.WithName("predicates").WithName("UpdateDebug") 30 | 31 | type updateDebugPredicate struct{} 32 | 33 | func UpdateDebug() *updateDebugPredicate { 34 | return &updateDebugPredicate{} 35 | } 36 | 37 | var _ predicate.Predicate = &updateDebugPredicate{} 38 | 39 | // Create returns true if the Create event should be processed 40 | func (_ *updateDebugPredicate) Create(_ event.CreateEvent) bool { 41 | return true 42 | } 43 | 44 | // Delete returns true if the Delete event should be processed 45 | func (_ *updateDebugPredicate) Delete(_ event.DeleteEvent) bool { 46 | return true 47 | } 48 | 49 | // Update returns true if the Update event should be processed 50 | func (_ *updateDebugPredicate) Update(evt event.UpdateEvent) bool { 51 | if os.Getenv("DEBUG_UPDATE") == "true" { 52 | obj := fmt.Sprintf("%s/%s", evt.ObjectNew.GetNamespace(), evt.ObjectNew.GetName()) 53 | diff, err := client.MergeFrom(evt.ObjectOld).Data(evt.ObjectNew) 54 | if err != nil { 55 | updateDebugLog.Info("error generating diff", "err", err, "obj", obj) 56 | } else { 57 | updateDebugLog.Info("Update diff", "diff", string(diff), "obj", obj) 58 | } 59 | } 60 | return true 61 | } 62 | 63 | // Generic returns true if the Generic event should be processed 64 | func (_ *updateDebugPredicate) Generic(_ event.GenericEvent) bool { 65 | return true 66 | } 67 | -------------------------------------------------------------------------------- /randstring/randstring.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Noah Kantrowitz 3 | Copyright 2019 Ridecell, Inc. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package randstring 19 | 20 | import ( 21 | "crypto/rand" 22 | "encoding/base64" 23 | ) 24 | 25 | var RandEncoding = base64.NewEncoding("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl").WithPadding(base64.NoPadding) 26 | 27 | func RandomBytes(size int) ([]byte, error) { 28 | raw := make([]byte, size) 29 | _, err := rand.Read(raw) 30 | if err != nil { 31 | return nil, err 32 | } 33 | out := make([]byte, RandEncoding.EncodedLen(size)) 34 | RandEncoding.Encode(out, raw) 35 | return out, nil 36 | } 37 | 38 | func RandomString(size int) (string, error) { 39 | randString, err := RandomBytes(size) 40 | return string(randString), err 41 | } 42 | 43 | func MustRandomBytes(size int) []byte { 44 | out, err := RandomBytes(size) 45 | if err != nil { 46 | panic(err) 47 | } 48 | return out 49 | } 50 | 51 | func MustRandomString(size int) string { 52 | out, err := RandomString(size) 53 | if err != nil { 54 | panic(err) 55 | } 56 | return out 57 | } 58 | -------------------------------------------------------------------------------- /templates/filesystem.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Noah Kantrowitz 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package templates 18 | 19 | import ( 20 | "fmt" 21 | "net/http" 22 | "os" 23 | "path/filepath" 24 | ) 25 | 26 | type filter struct { 27 | glob string 28 | shouldMatch bool 29 | } 30 | 31 | type FilteredFileSystem struct { 32 | fs http.FileSystem 33 | filters []*filter 34 | } 35 | 36 | type FilteredFile struct { 37 | http.File 38 | filters []*filter 39 | } 40 | 41 | func allowedByFilters(name string, filters []*filter) error { 42 | for _, f := range filters { 43 | matches, err := filepath.Match(f.glob, name[1:]) 44 | if err != nil { 45 | return err 46 | } 47 | if matches != f.shouldMatch { 48 | return fmt.Errorf("Does not match filter %s", f.glob) 49 | } 50 | } 51 | return nil 52 | } 53 | 54 | func NewFilteredFileSystem(fs http.FileSystem) *FilteredFileSystem { 55 | return &FilteredFileSystem{fs: fs, filters: []*filter{}} 56 | } 57 | 58 | func (ffs *FilteredFileSystem) Include(glob string) *FilteredFileSystem { 59 | return ffs.addFilter(glob, true) 60 | } 61 | 62 | func (ffs *FilteredFileSystem) Exclude(glob string) *FilteredFileSystem { 63 | return ffs.addFilter(glob, false) 64 | } 65 | 66 | func (ffs *FilteredFileSystem) addFilter(glob string, shouldMatch bool) *FilteredFileSystem { 67 | filters := make([]*filter, len(ffs.filters), len(ffs.filters)+1) 68 | copy(filters, ffs.filters) 69 | filters = append(filters, &filter{glob: glob, shouldMatch: shouldMatch}) 70 | return &FilteredFileSystem{ 71 | fs: ffs.fs, 72 | filters: filters, 73 | } 74 | } 75 | 76 | func (ffs *FilteredFileSystem) Open(name string) (http.File, error) { 77 | file, err := ffs.fs.Open(name) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | err = allowedByFilters(name, ffs.filters) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | return &FilteredFile{File: file, filters: ffs.filters}, nil 88 | } 89 | 90 | func (ff *FilteredFile) Readdir(count int) ([]os.FileInfo, error) { 91 | fis, err := ff.File.Readdir(count) 92 | if err != nil { 93 | return nil, err 94 | } 95 | out := make([]os.FileInfo, 0, len(fis)) 96 | for _, fi := range fis { 97 | if allowedByFilters(fi.Name(), ff.filters) == nil { 98 | out = append(out, fi) 99 | } 100 | } 101 | return out, nil 102 | } 103 | 104 | // Check interface compliance. 105 | var _ http.FileSystem = &FilteredFileSystem{} 106 | var _ http.File = &FilteredFile{} 107 | -------------------------------------------------------------------------------- /templates/filesystem_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Noah Kantrowitz 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package templates_test 18 | 19 | import ( 20 | "net/http" 21 | 22 | . "github.com/onsi/ginkgo" 23 | . "github.com/onsi/gomega" 24 | 25 | // . "github.com/onsi/gomega/gstruct" 26 | 27 | "github.com/coderanger/controller-utils/templates" 28 | ) 29 | 30 | var testFilteredTemplates *templates.FilteredFileSystem = templates.NewFilteredFileSystem(http.Dir("test_templates")) 31 | 32 | var _ = Describe("FilteredFileSystem", func() { 33 | Context("no filters", func() { 34 | It("lists all files", func() { 35 | f, err := testFilteredTemplates.Open("/") 36 | Expect(err).ToNot(HaveOccurred()) 37 | fis, err := f.Readdir(0) 38 | Expect(err).ToNot(HaveOccurred()) 39 | Expect(fis).To(HaveLen(6)) 40 | }) 41 | 42 | It("can read test.txt", func() { 43 | _, err := testFilteredTemplates.Open("/test.txt") 44 | Expect(err).ToNot(HaveOccurred()) 45 | }) 46 | 47 | It("can read test1.yml.tpl", func() { 48 | _, err := testFilteredTemplates.Open("/test1.yml.tpl") 49 | Expect(err).ToNot(HaveOccurred()) 50 | }) 51 | }) 52 | 53 | Context("an exclude filter", func() { 54 | exclude := testFilteredTemplates.Exclude("*.txt") 55 | 56 | It("lists the correct files", func() { 57 | f, err := exclude.Open("/") 58 | Expect(err).ToNot(HaveOccurred()) 59 | fis, err := f.Readdir(0) 60 | Expect(err).ToNot(HaveOccurred()) 61 | Expect(fis).To(HaveLen(5)) 62 | }) 63 | 64 | It("cannot read test.txt", func() { 65 | _, err := exclude.Open("/test.txt") 66 | Expect(err).To(HaveOccurred()) 67 | }) 68 | 69 | It("can read test1.yml.tpl", func() { 70 | _, err := exclude.Open("/test1.yml.tpl") 71 | Expect(err).ToNot(HaveOccurred()) 72 | }) 73 | }) 74 | 75 | Context("an include filter", func() { 76 | include := testFilteredTemplates.Include("*.txt") 77 | 78 | It("lists the correct files", func() { 79 | _, err := include.Open("/") 80 | Expect(err).To(HaveOccurred()) 81 | }) 82 | 83 | It("can read test.txt", func() { 84 | _, err := include.Open("/test.txt") 85 | Expect(err).ToNot(HaveOccurred()) 86 | }) 87 | 88 | It("cannot read test1.yml.tpl", func() { 89 | _, err := include.Open("/test1.yml.tpl") 90 | Expect(err).To(HaveOccurred()) 91 | }) 92 | }) 93 | }) 94 | -------------------------------------------------------------------------------- /templates/templates.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Noah Kantrowitz 3 | Copyright 2018-2019 Ridecell, Inc. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package templates 19 | 20 | import ( 21 | "bytes" 22 | "errors" 23 | "fmt" 24 | "net/http" 25 | "path" 26 | "reflect" 27 | "text/template" 28 | 29 | "github.com/Masterminds/sprig" 30 | "github.com/shurcooL/httpfs/path/vfspath" 31 | "github.com/shurcooL/httpfs/vfsutil" 32 | "gopkg.in/yaml.v2" 33 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 34 | "k8s.io/client-go/kubernetes/scheme" 35 | "sigs.k8s.io/controller-runtime/pkg/client" 36 | ) 37 | 38 | func parseTemplate(fs http.FileSystem, filename string) (*template.Template, error) { 39 | if fs == nil { 40 | return nil, errors.New("template filesystem not set") 41 | } 42 | 43 | // Wrote this because if statements with pointers don't work how you'd think they would 44 | customFuncMap := template.FuncMap{ 45 | "deref": func(input interface{}) interface{} { 46 | val := reflect.ValueOf(input) 47 | if val.IsNil() { 48 | return nil 49 | } 50 | return val.Elem().Interface() 51 | }, 52 | } 53 | 54 | // Create a template object. 55 | tmpl := template.New(path.Base(filename)).Funcs(sprig.TxtFuncMap()).Funcs(customFuncMap) 56 | 57 | // Parse any helpers if present. 58 | helpers, err := vfspath.Glob(fs, "helpers/*.tpl") 59 | if err != nil { 60 | return nil, err 61 | } 62 | for _, helperFilename := range helpers { 63 | fileBytes, err := vfsutil.ReadFile(fs, helperFilename) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | _, err = tmpl.Parse(string(fileBytes)) 69 | if err != nil { 70 | return nil, err 71 | } 72 | } 73 | 74 | // Parse the main template. 75 | fileBytes, err := vfsutil.ReadFile(fs, filename) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | _, err = tmpl.Parse(string(fileBytes)) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | return tmpl, nil 86 | } 87 | 88 | func renderTemplate(tmpl *template.Template, data interface{}) ([]byte, error) { 89 | var buffer bytes.Buffer 90 | err := tmpl.Execute(&buffer, data) 91 | if err != nil { 92 | return []byte{}, err 93 | } 94 | 95 | return buffer.Bytes(), nil 96 | } 97 | 98 | // Parse the rendered data into an object. The caller has to cast it from a 99 | // core.Object into the correct type. 100 | func parseObject(rawObject []byte) (client.Object, error) { 101 | obj, _, err := scheme.Codecs.UniversalDeserializer().Decode(rawObject, nil, nil) 102 | if err != nil { 103 | return nil, err 104 | } 105 | coreObj, ok := obj.(client.Object) 106 | if !ok { 107 | return nil, errors.New("unable to cast to core.Object") 108 | } 109 | return coreObj, nil 110 | } 111 | 112 | func castArray(in []interface{}) []interface{} { 113 | result := make([]interface{}, len(in)) 114 | for i, v := range in { 115 | result[i] = castValue(v) 116 | } 117 | return result 118 | } 119 | 120 | func castMap(in map[interface{}]interface{}) map[string]interface{} { 121 | result := make(map[string]interface{}) 122 | for k, v := range in { 123 | kString, ok := k.(string) 124 | if !ok { 125 | kString = fmt.Sprintf("%v", k) 126 | } 127 | result[kString] = castValue(v) 128 | } 129 | return result 130 | } 131 | 132 | func castValue(v interface{}) interface{} { 133 | switch v := v.(type) { 134 | case []interface{}: 135 | return castArray(v) 136 | case map[interface{}]interface{}: 137 | return castMap(v) 138 | default: 139 | return v 140 | } 141 | } 142 | 143 | // Parse the rendered data into an Unstructured. 144 | func parseUnstructured(rawObject []byte) (client.Object, error) { 145 | data := map[interface{}]interface{}{} 146 | err := yaml.Unmarshal(rawObject, data) 147 | if err != nil { 148 | return nil, err 149 | } 150 | return &unstructured.Unstructured{Object: castMap(data)}, nil 151 | } 152 | 153 | func Get(fs http.FileSystem, filename string, unstructured bool, data interface{}) (client.Object, error) { 154 | tmpl, err := parseTemplate(fs, filename) 155 | if err != nil { 156 | return nil, err 157 | } 158 | out, err := renderTemplate(tmpl, data) 159 | if err != nil { 160 | return nil, err 161 | } 162 | var obj client.Object 163 | if unstructured { 164 | obj, err = parseUnstructured(out) 165 | } else { 166 | obj, err = parseObject(out) 167 | } 168 | if err != nil { 169 | return nil, err 170 | } 171 | return obj, nil 172 | } 173 | -------------------------------------------------------------------------------- /templates/templates_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Noah Kantrowitz 3 | Copyright 2018-2019 Ridecell, Inc. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package templates_test 19 | 20 | import ( 21 | "testing" 22 | 23 | "github.com/onsi/ginkgo" 24 | "github.com/onsi/gomega" 25 | ) 26 | 27 | func TestTemplates(t *testing.T) { 28 | gomega.RegisterFailHandler(ginkgo.Fail) 29 | ginkgo.RunSpecs(t, "Templates Suite") 30 | } 31 | -------------------------------------------------------------------------------- /templates/templates_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Noah Kantrowitz 3 | Copyright 2018-2019 Ridecell, Inc. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package templates_test 19 | 20 | import ( 21 | "net/http" 22 | 23 | . "github.com/onsi/ginkgo" 24 | . "github.com/onsi/gomega" 25 | . "github.com/onsi/gomega/gstruct" 26 | appsv1 "k8s.io/api/apps/v1" 27 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 28 | 29 | "github.com/coderanger/controller-utils/templates" 30 | ) 31 | 32 | var testTemplates http.FileSystem = http.Dir("test_templates") 33 | 34 | var _ = Describe("Templates", func() { 35 | Context("a simple template", func() { 36 | It("should render the Deployment", func() { 37 | rawObject, err := templates.Get(testTemplates, "test1.yml.tpl", false, struct{}{}) 38 | Expect(err).ToNot(HaveOccurred()) 39 | deployment, ok := rawObject.(*appsv1.Deployment) 40 | Expect(ok).To(BeTrue()) 41 | Expect(deployment.Name).To(Equal("test")) 42 | Expect(deployment.Spec.Replicas).To(PointTo(BeEquivalentTo(1))) 43 | }) 44 | }) 45 | 46 | Context("a helper template", func() { 47 | It("should render the Deployment", func() { 48 | rawObject, err := templates.Get(testTemplates, "test2.yml.tpl", false, struct{}{}) 49 | Expect(err).ToNot(HaveOccurred()) 50 | deployment, ok := rawObject.(*appsv1.Deployment) 51 | Expect(ok).To(BeTrue()) 52 | Expect(deployment.Name).To(Equal("test-two")) 53 | Expect(deployment.Spec.Replicas).To(PointTo(BeEquivalentTo(1))) 54 | }) 55 | }) 56 | 57 | Context("a template variable", func() { 58 | It("should render the Deployment", func() { 59 | rawObject, err := templates.Get(testTemplates, "test3.yml.tpl", false, struct{ Name string }{Name: "tres"}) 60 | Expect(err).ToNot(HaveOccurred()) 61 | deployment, ok := rawObject.(*appsv1.Deployment) 62 | Expect(ok).To(BeTrue()) 63 | Expect(deployment.Name).To(Equal("test-tres")) 64 | Expect(deployment.Spec.Replicas).To(PointTo(BeEquivalentTo(1))) 65 | }) 66 | }) 67 | 68 | Context("unstructured mode", func() { 69 | It("should render the Deployment", func() { 70 | rawObject, err := templates.Get(testTemplates, "test1.yml.tpl", true, struct{}{}) 71 | Expect(err).ToNot(HaveOccurred()) 72 | _, ok := rawObject.(*appsv1.Deployment) 73 | Expect(ok).To(BeFalse()) 74 | obj, ok := rawObject.(*unstructured.Unstructured) 75 | Expect(ok).To(BeTrue()) 76 | Expect(obj.GetAPIVersion()).To(Equal("apps/v1")) 77 | Expect(obj.GetKind()).To(Equal("Deployment")) 78 | Expect(obj.GetName()).To(Equal("test")) 79 | Expect(obj.GetNamespace()).To(Equal("default")) 80 | }) 81 | 82 | It("handles an empty template", func() { 83 | rawObject, err := templates.Get(testTemplates, "empty.yml", true, nil) 84 | Expect(err).ToNot(HaveOccurred()) 85 | obj, ok := rawObject.(*unstructured.Unstructured) 86 | Expect(ok).To(BeTrue()) 87 | Expect(obj.UnstructuredContent()).To(HaveLen(0)) 88 | }) 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /templates/test_templates/empty.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderanger/controller-utils/92ba5c80e3fb4fd5e300853e6d001b9d71bedfe6/templates/test_templates/empty.yml -------------------------------------------------------------------------------- /templates/test_templates/helpers/deployment.yml.tpl: -------------------------------------------------------------------------------- 1 | {{ define "deployment" }} 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: test-{{ block "componentName" . }}{{ end }} 6 | spec: 7 | replicas: {{ block "replicas" . }}1{{ end }} 8 | selector: 9 | matchLabels: 10 | app: test 11 | template: 12 | metadata: 13 | labels: 14 | app: test 15 | spec: 16 | containers: 17 | - name: default 18 | image: test 19 | {{ end }} 20 | -------------------------------------------------------------------------------- /templates/test_templates/test.txt: -------------------------------------------------------------------------------- 1 | Hello world 2 | -------------------------------------------------------------------------------- /templates/test_templates/test1.yml.tpl: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: test 5 | namespace: default 6 | spec: 7 | replicas: {{ block "replicas" . }}1{{ end }} 8 | selector: 9 | matchLabels: 10 | app: test 11 | template: 12 | metadata: 13 | labels: 14 | app: test 15 | spec: 16 | containers: 17 | - name: default 18 | image: test 19 | -------------------------------------------------------------------------------- /templates/test_templates/test2.yml.tpl: -------------------------------------------------------------------------------- 1 | {{ define "componentName" }}two{{ end }} 2 | {{ template "deployment" . }} 3 | -------------------------------------------------------------------------------- /templates/test_templates/test3.yml.tpl: -------------------------------------------------------------------------------- 1 | {{ define "componentName" }}{{ .Name }}{{ end }} 2 | {{ template "deployment" . }} 3 | -------------------------------------------------------------------------------- /tests/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Noah Kantrowitz 3 | Copyright 2019 Ridecell, Inc. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package tests 19 | 20 | import ( 21 | "context" 22 | "reflect" 23 | "time" 24 | 25 | "github.com/onsi/gomega" 26 | gtypes "github.com/onsi/gomega/types" 27 | "github.com/pkg/errors" 28 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 | "k8s.io/apimachinery/pkg/types" 30 | "sigs.k8s.io/controller-runtime/pkg/client" 31 | ) 32 | 33 | // The default timeout for EventuallyGet(). 34 | var DefaultTimeout = 30 * time.Second 35 | 36 | // Implementation to match controller-runtime's client.Client interface. 37 | type testClient struct { 38 | client client.Client 39 | namespace string 40 | } 41 | 42 | type testStatusClient struct { 43 | client client.StatusWriter 44 | namespace string 45 | } 46 | 47 | func defaultNamespace(obj client.Object, namespace string) { 48 | metaobj := obj.(metav1.Object) 49 | if namespace != "" && metaobj.GetNamespace() == "" { 50 | metaobj.SetNamespace(namespace) 51 | } 52 | } 53 | 54 | // Implementation used by Get and GetName to keep the stack depth the same. 55 | func (c *testClient) get(key client.ObjectKey, obj client.Object) { 56 | if c.namespace != "" && key.Namespace == "" { 57 | key.Namespace = c.namespace 58 | } 59 | err := c.client.Get(context.Background(), key, obj) 60 | gomega.ExpectWithOffset(2, err).ToNot(gomega.HaveOccurred()) 61 | } 62 | 63 | func (c *testClient) Get(key client.ObjectKey, obj client.Object) { 64 | c.get(key, obj) 65 | } 66 | 67 | func (c *testClient) GetName(name string, obj client.Object) { 68 | gomega.ExpectWithOffset(1, c.namespace).ToNot(gomega.Equal(""), "Test client namespace not set") 69 | key := types.NamespacedName{Name: name, Namespace: c.namespace} 70 | c.get(key, obj) 71 | } 72 | 73 | func (c *testClient) List(list client.ObjectList, opts ...client.ListOption) { 74 | err := c.client.List(context.Background(), list, opts...) 75 | gomega.ExpectWithOffset(1, err).ToNot(gomega.HaveOccurred()) 76 | } 77 | 78 | func (c *testClient) Create(obj client.Object) { 79 | defaultNamespace(obj, c.namespace) 80 | err := c.client.Create(context.Background(), obj) 81 | gomega.ExpectWithOffset(1, err).ToNot(gomega.HaveOccurred()) 82 | } 83 | 84 | func (c *testClient) Delete(obj client.Object, opts ...client.DeleteOption) { 85 | defaultNamespace(obj, c.namespace) 86 | err := c.client.Delete(context.Background(), obj, opts...) 87 | gomega.ExpectWithOffset(1, err).ToNot(gomega.HaveOccurred()) 88 | } 89 | 90 | func (c *testClient) Update(obj client.Object) { 91 | defaultNamespace(obj, c.namespace) 92 | err := c.client.Update(context.Background(), obj) 93 | gomega.ExpectWithOffset(1, err).ToNot(gomega.HaveOccurred()) 94 | } 95 | 96 | func (c *testClient) Patch(obj client.Object, patch client.Patch, opts ...client.PatchOption) { 97 | defaultNamespace(obj, c.namespace) 98 | err := c.client.Patch(context.Background(), obj, patch, opts...) 99 | gomega.ExpectWithOffset(1, err).ToNot(gomega.HaveOccurred()) 100 | } 101 | 102 | // Implementation to match StatusClient. 103 | func (c *testClient) Status() *testStatusClient { 104 | return &testStatusClient{client: c.client.Status(), namespace: c.namespace} 105 | } 106 | 107 | func (c *testStatusClient) Update(obj client.Object) { 108 | defaultNamespace(obj, c.namespace) 109 | err := c.client.Update(context.Background(), obj) 110 | gomega.ExpectWithOffset(1, err).ToNot(gomega.HaveOccurred()) 111 | } 112 | 113 | func (c *testStatusClient) Patch(obj client.Object, patch client.Patch, opts ...client.PatchOption) { 114 | defaultNamespace(obj, c.namespace) 115 | err := c.client.Patch(context.Background(), obj, patch, opts...) 116 | gomega.ExpectWithOffset(1, err).ToNot(gomega.HaveOccurred()) 117 | } 118 | 119 | // Flexible helper, mostly used for waiting for an object to be available. 120 | type eventuallyGetOptions struct { 121 | timeout time.Duration 122 | valueGetter EventuallyGetValueGetter 123 | matcher gtypes.GomegaMatcher 124 | } 125 | 126 | type eventuallyGetOptionsSetter func(*eventuallyGetOptions) 127 | type EventuallyGetValueGetter func(client.Object) (interface{}, error) 128 | 129 | // Set the timeout to a non-default value for EventuallyGet(). 130 | func (_ *testClient) EventuallyTimeout(timeout time.Duration) eventuallyGetOptionsSetter { 131 | return func(o *eventuallyGetOptions) { 132 | o.timeout = timeout 133 | } 134 | } 135 | 136 | // Set a value getter, to poll until the requested value matches. 137 | func (_ *testClient) EventuallyValue(matcher gtypes.GomegaMatcher, getter EventuallyGetValueGetter) eventuallyGetOptionsSetter { 138 | return func(o *eventuallyGetOptions) { 139 | o.matcher = matcher 140 | o.valueGetter = getter 141 | } 142 | } 143 | 144 | // A common case of a value getter for status conditions. 145 | func (c *testClient) EventuallyCondition(conditionType string, status string) eventuallyGetOptionsSetter { 146 | return c.EventuallyValue(gomega.Equal(status), func(obj client.Object) (interface{}, error) { 147 | // Yes using reflect is kind of gross but it's test-only code so meh. Some day this can be a bit less reflect-y and use metav1.Condition, when everything is on that. 148 | conditions := reflect.ValueOf(obj).Elem().FieldByName("Status").FieldByName("Conditions") 149 | count := conditions.Len() 150 | for i := 0; i < count; i++ { 151 | cond := conditions.Index(i) 152 | if cond.FieldByName("Type").String() == conditionType { 153 | return cond.FieldByName("Status").String(), nil 154 | } 155 | } 156 | return nil, errors.Errorf("Condition type %s not found", conditionType) 157 | }) 158 | } 159 | 160 | // Even more common case of Ready and True. 161 | func (c *testClient) EventuallyReady() eventuallyGetOptionsSetter { 162 | return c.EventuallyCondition("Ready", "True") 163 | } 164 | 165 | // Implementation used by EventuallyGet and EventuallyGetName, to keep the stack depth the same. 166 | func (c *testClient) eventuallyGet(key client.ObjectKey, obj client.Object, optSetters ...eventuallyGetOptionsSetter) { 167 | if c.namespace != "" && key.Namespace == "" { 168 | key.Namespace = c.namespace 169 | } 170 | opts := eventuallyGetOptions{timeout: DefaultTimeout} 171 | for _, optSetter := range optSetters { 172 | optSetter(&opts) 173 | } 174 | 175 | if opts.valueGetter != nil { 176 | gomega.EventuallyWithOffset(2, func() (interface{}, error) { 177 | var value interface{} 178 | err := c.client.Get(context.Background(), key, obj) 179 | if err == nil { 180 | value, err = opts.valueGetter(obj) 181 | } 182 | return value, err 183 | }, opts.timeout).Should(opts.matcher) 184 | } else { 185 | gomega.EventuallyWithOffset(2, func() error { 186 | err := c.client.Get(context.Background(), key, obj) 187 | return err 188 | }, opts.timeout).Should(gomega.Succeed()) 189 | } 190 | } 191 | 192 | // Like a normal Get but run in a loop. By default it will wait until the call succeeds, but can optionally wait for a value. 193 | func (c *testClient) EventuallyGet(key client.ObjectKey, obj client.Object, optSetters ...eventuallyGetOptionsSetter) { 194 | c.eventuallyGet(key, obj, optSetters...) 195 | } 196 | 197 | // EventuallyGet but taking just a name. 198 | func (c *testClient) EventuallyGetName(name string, obj client.Object, optSetters ...eventuallyGetOptionsSetter) { 199 | c.eventuallyGet(types.NamespacedName{Name: name}, obj, optSetters...) 200 | } 201 | -------------------------------------------------------------------------------- /tests/functional.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Noah Kantrowitz 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tests 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "math/rand" 23 | "path/filepath" 24 | "strings" 25 | "time" 26 | 27 | "github.com/onsi/gomega" 28 | "github.com/pkg/errors" 29 | "github.com/prometheus/client_golang/prometheus" 30 | corev1 "k8s.io/api/core/v1" 31 | apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 32 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 34 | "k8s.io/apimachinery/pkg/runtime" 35 | "k8s.io/client-go/kubernetes/scheme" 36 | "k8s.io/client-go/rest" 37 | ctrl "sigs.k8s.io/controller-runtime" 38 | "sigs.k8s.io/controller-runtime/pkg/client" 39 | "sigs.k8s.io/controller-runtime/pkg/envtest" 40 | "sigs.k8s.io/controller-runtime/pkg/manager" 41 | "sigs.k8s.io/controller-runtime/pkg/metrics" 42 | "sigs.k8s.io/yaml" 43 | 44 | "github.com/coderanger/controller-utils/randstring" 45 | ) 46 | 47 | type schemeAdder func(*runtime.Scheme) error 48 | type managerAdder func(ctrl.Manager) error 49 | 50 | type functionalBuilder struct { 51 | crdPaths []string 52 | crds []*apiextv1.CustomResourceDefinition 53 | webhookPaths []string 54 | apis []schemeAdder 55 | externalName *string 56 | } 57 | 58 | type FunctionalSuiteHelper struct { 59 | environment *envtest.Environment 60 | cfg *rest.Config 61 | external bool 62 | } 63 | 64 | type FunctionalHelper struct { 65 | managerCancel context.CancelFunc 66 | managerDone chan struct{} 67 | UncachedClient client.Client 68 | Client client.Client 69 | TestClient *testClient 70 | Namespace string 71 | namespaceObj *corev1.Namespace 72 | } 73 | 74 | func Functional() *functionalBuilder { 75 | return &functionalBuilder{} 76 | } 77 | 78 | func (b *functionalBuilder) CRDPath(path string) *functionalBuilder { 79 | b.crdPaths = append(b.crdPaths, path) 80 | return b 81 | } 82 | 83 | func (b *functionalBuilder) CRD(crd *apiextv1.CustomResourceDefinition) *functionalBuilder { 84 | b.crds = append(b.crds, crd) 85 | return b 86 | } 87 | 88 | func (b *functionalBuilder) WebhookPaths(path string) *functionalBuilder { 89 | b.webhookPaths = append(b.webhookPaths, path) 90 | return b 91 | } 92 | 93 | func (b *functionalBuilder) API(adder schemeAdder) *functionalBuilder { 94 | b.apis = append(b.apis, adder) 95 | return b 96 | } 97 | 98 | func (b *functionalBuilder) UseExistingCluster(externalName string) *functionalBuilder { 99 | b.externalName = &externalName 100 | return b 101 | } 102 | 103 | func (b *functionalBuilder) Build() (*FunctionalSuiteHelper, error) { 104 | helper := &FunctionalSuiteHelper{} 105 | // Set up default paths for standard kubebuilder usage. 106 | if len(b.crdPaths) == 0 { 107 | b.crdPaths = append(b.crdPaths, filepath.Join("..", "config", "crd", "bases")) 108 | } 109 | var defaultWebhookPaths bool 110 | if len(b.webhookPaths) == 0 { 111 | b.webhookPaths = append(b.webhookPaths, filepath.Join("..", "config", "webhook")) 112 | defaultWebhookPaths = true 113 | } 114 | 115 | // Configure the test environment. 116 | helper.environment = &envtest.Environment{ 117 | CRDDirectoryPaths: b.crdPaths, 118 | CRDs: b.crds, 119 | WebhookInstallOptions: envtest.WebhookInstallOptions{ 120 | Paths: b.webhookPaths, 121 | IgnoreErrorIfPathMissing: defaultWebhookPaths, 122 | }, 123 | } 124 | if b.externalName != nil { 125 | boolp := true 126 | helper.environment.UseExistingCluster = &boolp 127 | helper.environment.WebhookInstallOptions.LocalServingHost = "0.0.0.0" 128 | helper.environment.WebhookInstallOptions.LocalServingHostExternalName = *b.externalName 129 | helper.external = true 130 | } 131 | 132 | // Initialze the RNG. 133 | rand.Seed(time.Now().UnixNano()) 134 | 135 | // Start the environment. 136 | var err error 137 | helper.cfg, err = helper.environment.Start() 138 | if err != nil { 139 | return nil, errors.Wrap(err, "error starting environment") 140 | } 141 | 142 | // Add all requested APIs to the global scheme. 143 | for _, adder := range b.apis { 144 | err = adder(scheme.Scheme) 145 | if err != nil { 146 | return nil, errors.Wrap(err, "error adding scheme") 147 | } 148 | } 149 | 150 | return helper, nil 151 | } 152 | 153 | func (b *functionalBuilder) MustBuild() *FunctionalSuiteHelper { 154 | fsh, err := b.Build() 155 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 156 | return fsh 157 | } 158 | 159 | func (fsh *FunctionalSuiteHelper) Stop() error { 160 | if fsh != nil && fsh.environment != nil { 161 | err := fsh.environment.Stop() 162 | if err != nil { 163 | return err 164 | } 165 | } 166 | return nil 167 | } 168 | 169 | func (fsh *FunctionalSuiteHelper) MustStop() { 170 | err := fsh.Stop() 171 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 172 | } 173 | 174 | func (fsh *FunctionalSuiteHelper) Start(controllers ...managerAdder) (*FunctionalHelper, error) { 175 | fh := &FunctionalHelper{} 176 | 177 | // Pick a randomize namespace so tests don't cross-talk as much. 178 | fh.Namespace = "test-" + randstring.MustRandomString(10) 179 | 180 | mgr, err := manager.New(fsh.cfg, manager.Options{ 181 | // Disable both listeners so tests don't raise a "Do you want to allow ... to listen" dialog on macOS. 182 | MetricsBindAddress: "0", 183 | HealthProbeBindAddress: "0", 184 | Namespace: fh.Namespace, 185 | Host: fsh.environment.WebhookInstallOptions.LocalServingHost, 186 | Port: fsh.environment.WebhookInstallOptions.LocalServingPort, 187 | CertDir: fsh.environment.WebhookInstallOptions.LocalServingCertDir, 188 | LeaderElection: false, 189 | }) 190 | if err != nil { 191 | return nil, errors.Wrap(err, "error creating manager") 192 | } 193 | 194 | // Add the requested controllers. 195 | for _, adder := range controllers { 196 | err := adder(mgr) 197 | if err != nil { 198 | return nil, errors.Wrap(err, "error adding controller") 199 | } 200 | } 201 | 202 | // Start the manager (in the background). 203 | ctx, cancel := context.WithCancel(context.Background()) 204 | fh.managerCancel = cancel 205 | fh.managerDone = make(chan struct{}) 206 | go func() { 207 | defer close(fh.managerDone) 208 | err := mgr.Start(ctx) 209 | if err != nil { 210 | panic(err) 211 | } 212 | }() 213 | 214 | // Grab the clients. 215 | fh.Client = mgr.GetClient() 216 | fh.UncachedClient, err = client.New(fsh.cfg, client.Options{Scheme: mgr.GetScheme()}) 217 | if err != nil { 218 | return nil, errors.Wrap(err, "error creating raw client") 219 | } 220 | 221 | // Create the actual random namespace. 222 | namespace := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: fh.Namespace}} 223 | err = fh.UncachedClient.Create(context.Background(), namespace) 224 | if err != nil { 225 | return nil, errors.Wrapf(err, "error creating test namespace %s", fh.Namespace) 226 | } 227 | if fsh.external { 228 | fh.namespaceObj = namespace 229 | } 230 | 231 | // Create a namespace-bound test client. 232 | fh.TestClient = &testClient{client: fh.Client, namespace: fh.Namespace} 233 | 234 | return fh, nil 235 | } 236 | 237 | func (fsh *FunctionalSuiteHelper) MustStart(controllers ...managerAdder) *FunctionalHelper { 238 | fh, err := fsh.Start(controllers...) 239 | gomega.Expect(err).ToNot(gomega.HaveOccurred()) 240 | return fh 241 | } 242 | 243 | func (fh *FunctionalHelper) Stop() error { 244 | // Clean up the namespace if using an extneral control plane. 245 | if fh.namespaceObj != nil { 246 | err := fh.UncachedClient.Delete(context.Background(), fh.namespaceObj) 247 | if err != nil { 248 | return err 249 | } 250 | } 251 | if fh != nil && fh.managerCancel != nil { 252 | fh.managerCancel() 253 | // TODO maybe replace this with my own timeout so it doesn't use Gomega. 254 | gomega.Eventually(fh.managerDone, 30*time.Second).Should(gomega.BeClosed()) 255 | } 256 | // TODO This is not needed in controller-runtime 0.6 or above, revisit. 257 | metrics.Registry = prometheus.NewRegistry() 258 | return nil 259 | } 260 | 261 | func (fh *FunctionalHelper) MustStop() { 262 | err := fh.Stop() 263 | gomega.Expect(err).ToNot(gomega.HaveOccurred()) 264 | } 265 | 266 | // Helper method to show a list of objects, used in AfterEach helpers. 267 | func (fh *FunctionalHelper) DebugList(listType runtime.Object) { 268 | gvks, unversioned, err := scheme.Scheme.ObjectKinds(listType) 269 | if err != nil { 270 | fmt.Printf("DebugList Error: %v", err) 271 | panic(err) 272 | } 273 | if unversioned || len(gvks) == 0 { 274 | fmt.Println("DebugList Error: Error getting GVKs") 275 | panic("Error getting GVKs") 276 | } 277 | list := &unstructured.UnstructuredList{} 278 | list.SetGroupVersionKind(gvks[0]) 279 | 280 | err = fh.UncachedClient.List(context.Background(), list) 281 | if err != nil { 282 | fmt.Printf("DebugList Error: %v", err) 283 | panic(err) 284 | } 285 | 286 | output := map[string]interface{}{} 287 | for _, item := range list.Items { 288 | meta := item.Object["metadata"].(map[string]interface{}) 289 | if meta["namespace"].(string) == fh.Namespace { 290 | output[meta["name"].(string)] = item.Object 291 | } 292 | } 293 | outputBytes, err := yaml.Marshal(output) 294 | if err != nil { 295 | fmt.Printf("DebugList Error: %v", err) 296 | panic(err) 297 | } 298 | fmt.Printf("\n%s\n%s\n%s\n", gvks[0].Kind, strings.Repeat("=", len(gvks[0].Kind)), string(outputBytes)) 299 | } 300 | -------------------------------------------------------------------------------- /tests/matchers/conditions.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Noah Kantrowitz 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package matchers 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/onsi/gomega" 23 | 24 | "github.com/coderanger/controller-utils/conditions" 25 | "github.com/coderanger/controller-utils/core" 26 | "sigs.k8s.io/controller-runtime/pkg/client" 27 | ) 28 | 29 | type haveConditionMatcher struct { 30 | conditionType string 31 | status *string 32 | reason *string 33 | } 34 | 35 | func HaveCondition(conditionType string) *haveConditionMatcher { 36 | return &haveConditionMatcher{conditionType: conditionType} 37 | } 38 | 39 | func (matcher *haveConditionMatcher) WithStatus(status string) *haveConditionMatcher { 40 | matcher.status = &status 41 | return matcher 42 | } 43 | 44 | func (matcher *haveConditionMatcher) WithReason(reason string) *haveConditionMatcher { 45 | matcher.reason = &reason 46 | return matcher 47 | } 48 | 49 | func (matcher *haveConditionMatcher) Match(actual interface{}) (bool, error) { 50 | obj, ok := actual.(client.Object) 51 | if !ok { 52 | return false, fmt.Errorf("HaveCondition matcher expects a client.Object") 53 | } 54 | conds, err := core.GetConditionsFor(obj) 55 | if err != nil { 56 | return false, err 57 | } 58 | 59 | cond := conditions.FindStatusCondition(*conds, matcher.conditionType) 60 | if cond == nil { 61 | return false, nil 62 | } 63 | 64 | if matcher.status != nil { 65 | match, err := gomega.BeEquivalentTo(*matcher.status).Match(cond.Status) 66 | if !match || err != nil { 67 | return match, err 68 | } 69 | } 70 | 71 | if matcher.reason != nil { 72 | match, err := gomega.Equal(*matcher.reason).Match(cond.Reason) 73 | if !match || err != nil { 74 | return match, err 75 | } 76 | } 77 | 78 | return true, nil 79 | } 80 | 81 | func (matcher *haveConditionMatcher) FailureMessage(actual interface{}) string { 82 | return matcher.message(actual, true) 83 | } 84 | 85 | func (matcher *haveConditionMatcher) NegatedFailureMessage(actual interface{}) string { 86 | return matcher.message(actual, false) 87 | } 88 | 89 | func (matcher *haveConditionMatcher) message(actual interface{}, polarity bool) string { 90 | filters := "" 91 | if matcher.status != nil { 92 | filters += fmt.Sprintf(" with status %s", *matcher.status) 93 | } 94 | if matcher.reason != nil { 95 | filters += fmt.Sprintf(" with reason %s", *matcher.reason) 96 | } 97 | 98 | joiner := "" 99 | if !polarity { 100 | joiner = "not " 101 | } 102 | 103 | obj, ok := actual.(client.Object) 104 | if ok { 105 | conds, err := core.GetConditionsFor(obj) 106 | if err == nil { 107 | actual = *conds 108 | } 109 | } 110 | 111 | return fmt.Sprintf("Expected %#v to %shave condition %s%s", actual, joiner, matcher.conditionType, filters) 112 | } 113 | -------------------------------------------------------------------------------- /tests/unit.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Noah Kantrowitz 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tests 18 | 19 | import ( 20 | "context" 21 | "net/http" 22 | 23 | "github.com/onsi/gomega" 24 | "github.com/pkg/errors" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/apimachinery/pkg/runtime" 27 | "k8s.io/client-go/kubernetes/scheme" 28 | "k8s.io/client-go/tools/record" 29 | ctrl "sigs.k8s.io/controller-runtime" 30 | "sigs.k8s.io/controller-runtime/pkg/client" 31 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 32 | "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 33 | 34 | "github.com/coderanger/controller-utils/core" 35 | ) 36 | 37 | type unitBuilder struct { 38 | apis []schemeAdder 39 | templates http.FileSystem 40 | } 41 | 42 | type UnitSuiteHelper struct { 43 | scheme *runtime.Scheme 44 | templates http.FileSystem 45 | } 46 | 47 | type UnitHelper struct { 48 | Comp core.Component 49 | Client client.Client 50 | TestClient *testClient 51 | Object client.Object 52 | Events chan string 53 | Ctx *core.Context 54 | } 55 | 56 | func Unit() *unitBuilder { 57 | return &unitBuilder{} 58 | } 59 | 60 | func (b *unitBuilder) API(adder schemeAdder) *unitBuilder { 61 | b.apis = append(b.apis, adder) 62 | return b 63 | } 64 | 65 | func (b *unitBuilder) Templates(templates http.FileSystem) *unitBuilder { 66 | b.templates = templates 67 | return b 68 | } 69 | 70 | func (b *unitBuilder) Build() (*UnitSuiteHelper, error) { 71 | sch := runtime.NewScheme() 72 | 73 | // Register the default scheme things. 74 | err := scheme.AddToScheme(sch) 75 | if err != nil { 76 | return nil, errors.Wrap(err, "error adding default scheme") 77 | } 78 | 79 | // Add all requested APIs to the global scheme. 80 | for _, adder := range b.apis { 81 | err = adder(sch) 82 | if err != nil { 83 | return nil, errors.Wrap(err, "error adding scheme") 84 | } 85 | } 86 | 87 | return &UnitSuiteHelper{templates: b.templates, scheme: sch}, nil 88 | } 89 | 90 | func (b *unitBuilder) MustBuild() *UnitSuiteHelper { 91 | ush, err := b.Build() 92 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 93 | return ush 94 | } 95 | 96 | func (ush *UnitSuiteHelper) Setup(comp core.Component, obj client.Object) *UnitHelper { 97 | uh := &UnitHelper{Comp: comp} 98 | 99 | metaObj := obj.(metav1.Object) 100 | if metaObj.GetName() == "" { 101 | metaObj.SetName("testing") 102 | } 103 | if metaObj.GetNamespace() == "" { 104 | metaObj.SetNamespace("default") 105 | } 106 | uh.Object = obj 107 | 108 | uh.Client = fake.NewFakeClientWithScheme(ush.scheme, uh.Object) 109 | uh.TestClient = &testClient{client: uh.Client, namespace: metaObj.GetNamespace()} 110 | 111 | events := record.NewFakeRecorder(100) 112 | uh.Events = events.Events 113 | 114 | ctx := &core.Context{ 115 | Context: context.Background(), 116 | Object: uh.Object, 117 | Client: uh.Client, 118 | UncachedClient: uh.Client, 119 | Templates: ush.templates, 120 | FieldManager: "unit-tests", 121 | Scheme: ush.scheme, 122 | Data: core.ContextData{}, 123 | Events: events, 124 | Conditions: core.NewConditionsHelper(uh.Object), 125 | Log: ctrl.Log.WithName("component"), 126 | } 127 | uh.Ctx = ctx 128 | 129 | return uh 130 | } 131 | 132 | func (uh *UnitHelper) Reconcile() (core.Result, error) { 133 | defaulter, ok := uh.Object.(admission.Defaulter) 134 | if ok { 135 | defaulter.Default() 136 | } 137 | uh.TestClient.Update(uh.Object) 138 | res, err := uh.Comp.Reconcile(uh.Ctx) 139 | compErr := uh.Ctx.Conditions.Flush() 140 | if compErr != nil && err == nil { 141 | err = compErr 142 | } 143 | return res, err 144 | } 145 | 146 | func (uh *UnitHelper) MustReconcile() core.Result { 147 | res, err := uh.Reconcile() 148 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 149 | return res 150 | } 151 | 152 | func (uh *UnitHelper) Finalize() (core.Result, bool, error) { 153 | finalizer, ok := uh.Comp.(core.FinalizerComponent) 154 | if !ok { 155 | return core.Result{}, false, errors.New("component is not a finalizer") 156 | } 157 | defaulter, ok := uh.Object.(admission.Defaulter) 158 | if ok { 159 | defaulter.Default() 160 | } 161 | uh.TestClient.Update(uh.Object) 162 | res, done, err := finalizer.Finalize(uh.Ctx) 163 | compErr := uh.Ctx.Conditions.Flush() 164 | if compErr != nil && err == nil { 165 | err = compErr 166 | } 167 | return res, done, err 168 | } 169 | 170 | func (uh *UnitHelper) MustFinalize() (core.Result, bool) { 171 | res, done, err := uh.Finalize() 172 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 173 | return res, done 174 | } 175 | --------------------------------------------------------------------------------