├── .github ├── renovate.json └── workflows │ └── test.yaml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── Makefile ├── README.md ├── cmd └── deepcopy │ └── main.go ├── go.mod ├── go.sum ├── pkg ├── apply │ ├── apply.go │ ├── desiredset.go │ ├── desiredset_apply.go │ ├── desiredset_compare.go │ ├── desiredset_crud.go │ ├── desiredset_log.go │ ├── desiredset_owner.go │ ├── desiredset_process.go │ ├── objectset │ │ └── objectset.go │ ├── patch_style.go │ └── reconcilers.go ├── backend │ └── backend.go ├── conditions │ └── setter.go ├── data │ ├── generic.go │ └── map.go ├── deepcopy │ └── deepcopy.go ├── fields │ └── fields.go ├── handlers │ └── handlers.go ├── leader │ └── leader.go ├── log │ └── log.go ├── logrus │ └── init.go ├── mapper │ └── mapper.go ├── merr │ └── error.go ├── name │ └── name.go ├── randomtoken │ └── token.go ├── ratelimit │ └── none.go ├── restconfig │ ├── restconfig.go │ ├── scheme.go │ └── wait.go ├── router │ ├── client.go │ ├── finalizer.go │ ├── getorcreate.go │ ├── handler.go │ ├── handlers.go │ ├── healthz.go │ ├── matcher.go │ ├── response_wrapper.go │ ├── router.go │ ├── save.go │ ├── tester │ │ ├── generate.go │ │ ├── tester.go │ │ └── types.go │ ├── trigger.go │ └── types.go ├── runtime │ ├── backend.go │ ├── cached.go │ ├── clients.go │ ├── controller.go │ ├── copy.go │ ├── errorcontroller.go │ ├── multi │ │ ├── cache.go │ │ ├── client.go │ │ ├── restmapper.go │ │ ├── status.go │ │ └── subresource.go │ ├── sharedcontroller.go │ ├── sharedcontrollerfactory.go │ ├── sharedhandler.go │ └── transaction.go ├── typed │ ├── chan.go │ ├── funcs.go │ ├── map.go │ └── slice.go ├── uncached │ └── uncached.go ├── urlbuilder │ └── urlbuilder.go ├── version │ └── version.go ├── watcher │ └── watcher.go ├── webhook │ ├── match.go │ └── router.go └── yaml │ └── yaml.go └── router.go /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "labels": ["renovate"], 3 | "extends": [ 4 | "config:base", ":disableDependencyDashboard" 5 | ], 6 | "postUpdateOptions": ["gomodTidy"], 7 | "packageRules": [ 8 | { 9 | "matchPackagePatterns": [ 10 | "^golang.*" 11 | ], 12 | "groupName": "golang", 13 | "groupSlug": "golang" 14 | } 15 | ], 16 | "regexManagers": [ 17 | { 18 | "fileMatch": [ 19 | "^.github/workflows/.*" 20 | ], 21 | "matchStrings": [ 22 | "go-version: (?.*?)\n" 23 | ], 24 | "depNameTemplate": "golang", 25 | "datasourceTemplate": "docker" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: {} 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | with: 14 | fetch-depth: 0 15 | - uses: actions/setup-go@v4 16 | with: 17 | cache: false 18 | go-version: "1.21" 19 | - run: make setup-ci-env 20 | - run: make validate-ci 21 | - run: make validate 22 | - run: make test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | 4 | output: 5 | format: github-actions 6 | 7 | skip-files: 8 | - "*generated*.go$" 9 | 10 | linters: 11 | disable-all: true 12 | enable: 13 | - errcheck 14 | - gofmt 15 | - gosimple 16 | - govet 17 | - ineffassign 18 | - staticcheck 19 | - typecheck 20 | - thelper 21 | - unused 22 | fast: false -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | setup-ci-env: 2 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $$(go env GOPATH)/bin v1.55.2 3 | 4 | validate-ci: 5 | go generate 6 | go mod tidy 7 | if [ -n "$$(git status --porcelain --untracked-files=no)" ]; then \ 8 | git status --porcelain --untracked-files=no; \ 9 | echo "Encountered dirty repo!"; \ 10 | exit 1 \ 11 | ;fi 12 | 13 | validate: 14 | golangci-lint run 15 | 16 | test: 17 | go test ./... 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # baaah 2 | `baaah` is a controller framework born out of frustration. It strives to provide a simple interface for watching and updating Kubernetes objects. 3 | 4 | ## Usage 5 | The easiest way to get started with `baaah` is to create a default router and start registering handlers (or handler functions). 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "context" 12 | 13 | "github.com/acorn-io/baaah" 14 | "github.com/acorn-io/baaah/pkg/router" 15 | "k8s.io/client-go/kubernetes/scheme" 16 | appsv1 "k8s.io/api/apps/v1" 17 | corev1 "k8s.io/api/core/v1" 18 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 19 | ) 20 | 21 | func main() { 22 | r, err := baaah.DefaultRouter("my-router", scheme.Scheme) 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | r.Type(new(appsv1.Deployment)).HandlerFunc(handleDeployment) 28 | 29 | ctx := context.Background() 30 | err = r.Start(ctx) 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | <-ctx.Done() 36 | // Background context was canceled. Do whatever cleanup necessary. 37 | } 38 | 39 | func handleDeployment(req router.Request, resp router.Response) error { 40 | // Act on the deployment, which can be retrieved via req.Object 41 | // If you need to create new objects based on the deployment, use resp.Objects() 42 | // A req.Client and req.Ctx are provided for instances when you need to directly interact with the Kubernetes API. 43 | deployment := req.Object.(*appsv1.Deployment) 44 | 45 | secret := &corev1.Secret{ 46 | ObjectMeta: metav1.ObjectMeta{ 47 | Name: deployment.Name + "-key", 48 | Namespace: deployment.Namespace, 49 | }, 50 | Data: map[string][]byte{ 51 | "token": []byte("secret-key"), 52 | }, 53 | } 54 | 55 | resp.Objects(secret) 56 | return nil 57 | } 58 | ``` 59 | 60 | When using the `req.Client` and `resp.Objects` in your handlers, there are some niceties built-in to be aware of. 61 | 62 | When any object passed to `resp.Objects` changes, there is a "trigger" created under the hood that will ensure the object that created object goes through its handlers. For exampale, if the implementation of the deployment handle above created a secret for the deployment by passing the secret to `resp.Objects`, then everytime that secret changed, the deployment handler would be called with the deployment that created that secret. This is to ensure that the secret always has the correct/expected values. 63 | 64 | Similarly, anytime the `req.Client` is used, `baaah` will also create triggers for those objects. Meaning that if the deployment handler above used the client to list all secrets, then the deployment handlers would be triggered anytime any secret is created/updated. If the client was used to get a specific service, then the deployment handler would be triggered any time that specific service was changed. This is something to be mindful of to ensure you don't overload the Kubernetes API. Using selectors when listing or using get would be preferred. 65 | 66 | -------------------------------------------------------------------------------- /cmd/deepcopy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/acorn-io/baaah/pkg/deepcopy" 7 | ) 8 | 9 | func main() { 10 | deepcopy.Deepcopy(os.Args[1:]...) 11 | } 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/acorn-io/baaah 2 | 3 | go 1.23 4 | 5 | toolchain go1.23.2 6 | 7 | require ( 8 | github.com/bombsimon/logrusr/v4 v4.1.0 9 | github.com/google/uuid v1.6.0 10 | github.com/hexops/autogold/v2 v2.2.1 11 | github.com/moby/locker v1.0.1 12 | github.com/sirupsen/logrus v1.9.3 13 | github.com/stretchr/testify v1.9.0 14 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c 15 | golang.org/x/time v0.7.0 16 | gomodules.xyz/jsonpatch/v2 v2.4.0 17 | k8s.io/api v0.31.1 18 | k8s.io/apimachinery v0.31.1 19 | k8s.io/client-go v0.31.1 20 | k8s.io/klog/v2 v2.130.1 21 | k8s.io/kube-openapi v0.0.0-20241009091222-67ed5848f094 22 | k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 23 | sigs.k8s.io/controller-runtime v0.19.0 24 | sigs.k8s.io/controller-tools v0.16.4 25 | sigs.k8s.io/yaml v1.4.0 26 | ) 27 | 28 | require ( 29 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 30 | github.com/emicklei/go-restful/v3 v3.12.1 // indirect 31 | github.com/evanphx/json-patch v5.7.0+incompatible // indirect 32 | github.com/evanphx/json-patch/v5 v5.9.0 // indirect 33 | github.com/fatih/color v1.17.0 // indirect 34 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 35 | github.com/go-logr/logr v1.4.2 // indirect 36 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 37 | github.com/go-openapi/jsonreference v0.21.0 // indirect 38 | github.com/go-openapi/swag v0.23.0 // indirect 39 | github.com/gogo/protobuf v1.3.2 // indirect 40 | github.com/golang/protobuf v1.5.4 // indirect 41 | github.com/google/gnostic-models v0.6.8 // indirect 42 | github.com/google/go-cmp v0.6.0 // indirect 43 | github.com/google/gofuzz v1.2.0 // indirect 44 | github.com/hexops/autogold v1.3.1 // indirect 45 | github.com/hexops/gotextdiff v1.0.3 // indirect 46 | github.com/hexops/valast v1.4.4 // indirect 47 | github.com/imdario/mergo v0.3.16 // indirect 48 | github.com/josharian/intern v1.0.0 // indirect 49 | github.com/json-iterator/go v1.1.12 // indirect 50 | github.com/klauspost/compress v1.17.11 // indirect 51 | github.com/mailru/easyjson v0.7.7 // indirect 52 | github.com/mattn/go-colorable v0.1.13 // indirect 53 | github.com/mattn/go-isatty v0.0.20 // indirect 54 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 55 | github.com/modern-go/reflect2 v1.0.2 // indirect 56 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 57 | github.com/nightlyone/lockfile v1.0.0 // indirect 58 | github.com/pkg/errors v0.9.1 // indirect 59 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 60 | github.com/prometheus/client_golang v1.20.5 // indirect 61 | github.com/prometheus/common v0.60.0 // indirect 62 | github.com/spf13/pflag v1.0.5 // indirect 63 | github.com/x448/float16 v0.8.4 // indirect 64 | golang.org/x/mod v0.21.0 // indirect 65 | golang.org/x/net v0.30.0 // indirect 66 | golang.org/x/oauth2 v0.23.0 // indirect 67 | golang.org/x/sync v0.8.0 // indirect 68 | golang.org/x/sys v0.26.0 // indirect 69 | golang.org/x/term v0.25.0 // indirect 70 | golang.org/x/text v0.19.0 // indirect 71 | golang.org/x/tools v0.26.0 // indirect 72 | google.golang.org/protobuf v1.35.1 // indirect 73 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 74 | gopkg.in/inf.v0 v0.9.1 // indirect 75 | gopkg.in/yaml.v2 v2.4.0 // indirect 76 | gopkg.in/yaml.v3 v3.0.1 // indirect 77 | mvdan.cc/gofumpt v0.7.0 // indirect 78 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 79 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 80 | ) 81 | -------------------------------------------------------------------------------- /pkg/apply/apply.go: -------------------------------------------------------------------------------- 1 | package apply 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | "k8s.io/apimachinery/pkg/runtime/schema" 9 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 10 | ) 11 | 12 | const ( 13 | defaultNamespace = "default" 14 | ) 15 | 16 | var ( 17 | // validOwnerChange is a mapping of old to new subcontext 18 | // values that are allowed to change. The key is in the format of 19 | // "oldValue => newValue" 20 | // subcontext and the value is the old subcontext name. 21 | validOwnerChange = sync.Map{} 22 | ) 23 | 24 | func AddValidOwnerChange(oldSubcontext, newSubContext string) { 25 | validOwnerChange.Store(fmt.Sprintf("%s => %s", oldSubcontext, newSubContext), struct{}{}) 26 | } 27 | 28 | type Apply interface { 29 | Ensure(ctx context.Context, obj ...kclient.Object) error 30 | Apply(ctx context.Context, owner kclient.Object, objs ...kclient.Object) error 31 | WithOwnerSubContext(ownerSubContext string) Apply 32 | WithNamespace(ns string) Apply 33 | WithPruneGVKs(gvks ...schema.GroupVersionKind) Apply 34 | WithPruneTypes(gvks ...kclient.Object) Apply 35 | WithNoPrune() Apply 36 | 37 | FindOwner(ctx context.Context, obj kclient.Object) (kclient.Object, error) 38 | PurgeOrphan(ctx context.Context, obj kclient.Object) error 39 | } 40 | 41 | func Ensure(ctx context.Context, client kclient.Client, obj ...kclient.Object) error { 42 | return New(client).Ensure(ctx, obj...) 43 | } 44 | 45 | func New(c kclient.Client) Apply { 46 | return &apply{ 47 | client: c, 48 | reconcilers: defaultReconcilers, 49 | defaultNamespace: defaultNamespace, 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pkg/apply/desiredset.go: -------------------------------------------------------------------------------- 1 | package apply 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/acorn-io/baaah/pkg/apply/objectset" 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 9 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 10 | ) 11 | 12 | // reconciler return false if it did not handle this object 13 | type reconciler func(oldObj kclient.Object, newObj kclient.Object) (bool, error) 14 | 15 | type apply struct { 16 | ctx context.Context 17 | client kclient.Client 18 | defaultNamespace string 19 | listerNamespace string 20 | pruneTypes map[schema.GroupVersionKind]bool 21 | pruneObjects []kclient.Object 22 | reconcilers map[schema.GroupVersionKind]reconciler 23 | ownerSubContext string 24 | owner kclient.Object 25 | ownerGVK schema.GroupVersionKind 26 | ensure bool 27 | noPrune bool 28 | } 29 | 30 | func (a apply) Ensure(ctx context.Context, objs ...kclient.Object) error { 31 | a.ensure = true 32 | a.owner = nil 33 | a.ownerSubContext = "" 34 | return a.Apply(ctx, nil, objs...) 35 | } 36 | 37 | func (a apply) Apply(ctx context.Context, owner kclient.Object, objs ...kclient.Object) error { 38 | var newPruneGVKs []schema.GroupVersionKind 39 | for _, pruneObject := range a.pruneObjects { 40 | gvk, err := apiutil.GVKForObject(pruneObject, a.client.Scheme()) 41 | if err != nil { 42 | return err 43 | } 44 | newPruneGVKs = append(newPruneGVKs, gvk) 45 | } 46 | 47 | a = a.withPruneGVKs(newPruneGVKs...) 48 | a.ctx = ctx 49 | a.owner = owner 50 | if owner != nil { 51 | gvk, err := apiutil.GVKForObject(a.owner, a.client.Scheme()) 52 | if err != nil { 53 | return err 54 | } 55 | a.ownerGVK = gvk 56 | } 57 | os, err := objectset.NewObjectSet(a.client.Scheme(), objs...) 58 | if err != nil { 59 | return err 60 | } 61 | return a.apply(os) 62 | } 63 | 64 | func (a apply) WithNoPrune() Apply { 65 | a.noPrune = true 66 | return a 67 | } 68 | 69 | func (a apply) WithPruneTypes(objs ...kclient.Object) Apply { 70 | a.pruneObjects = append(a.pruneObjects, objs...) 71 | return a 72 | } 73 | 74 | // WithPruneGVKs uses a known listing of existing gvks to modify the the prune types to allow for deletion of objects 75 | func (a apply) WithPruneGVKs(gvks ...schema.GroupVersionKind) Apply { 76 | return a.withPruneGVKs(gvks...) 77 | } 78 | 79 | func (a apply) withPruneGVKs(gvks ...schema.GroupVersionKind) apply { 80 | pruneTypes := make(map[schema.GroupVersionKind]bool, len(gvks)) 81 | for k, v := range a.pruneTypes { 82 | pruneTypes[k] = v 83 | } 84 | for _, gvk := range gvks { 85 | pruneTypes[gvk] = true 86 | } 87 | a.pruneTypes = pruneTypes 88 | return a 89 | } 90 | 91 | func (a apply) WithNamespace(ns string) Apply { 92 | a.listerNamespace = ns 93 | a.defaultNamespace = ns 94 | return a 95 | } 96 | 97 | func (a apply) WithOwnerSubContext(ownerSubContext string) Apply { 98 | a.ownerSubContext = ownerSubContext 99 | return a 100 | } 101 | -------------------------------------------------------------------------------- /pkg/apply/desiredset_apply.go: -------------------------------------------------------------------------------- 1 | package apply 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/hex" 6 | "fmt" 7 | 8 | "github.com/acorn-io/baaah/pkg/apply/objectset" 9 | "github.com/acorn-io/baaah/pkg/merr" 10 | "k8s.io/apimachinery/pkg/api/meta" 11 | "k8s.io/apimachinery/pkg/labels" 12 | "k8s.io/apimachinery/pkg/runtime" 13 | "k8s.io/apimachinery/pkg/runtime/schema" 14 | "k8s.io/apimachinery/pkg/selection" 15 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 16 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 17 | ) 18 | 19 | const ( 20 | LabelPrefix = "apply.acorn.io/" 21 | 22 | LabelSubContext = LabelPrefix + "owner-sub-context" 23 | LabelGVK = LabelPrefix + "owner-gvk" 24 | LabelName = LabelPrefix + "owner-name" 25 | LabelNamespace = LabelPrefix + "owner-namespace" 26 | LabelHash = LabelPrefix + "hash" 27 | 28 | AnnotationPrune = LabelPrefix + "prune" 29 | AnnotationCreate = LabelPrefix + "create" 30 | AnnotationUpdate = LabelPrefix + "update" 31 | ) 32 | 33 | var ( 34 | hashOrder = []string{ 35 | LabelSubContext, 36 | LabelGVK, 37 | LabelName, 38 | LabelNamespace, 39 | } 40 | ) 41 | 42 | func (a *apply) apply(objs *objectset.ObjectSet) error { 43 | // retain the original order 44 | gvkOrder := objs.GVKOrder(a.knownGVK()...) 45 | 46 | labelSet, annotationSet, err := GetLabelsAndAnnotations(a.client.Scheme(), a.ownerSubContext, a.owner) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | objs, err = a.injectLabelsAndAnnotations(objs, labelSet, annotationSet) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | debugID := a.debugID() 57 | sel, err := GetSelector(labelSet) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | var errs []error 63 | for _, gvk := range gvkOrder { 64 | err := a.process(debugID, sel, gvk, objs) 65 | if err != nil { 66 | errs = append(errs, err) 67 | } 68 | } 69 | 70 | return merr.NewErrors(errs...) 71 | } 72 | 73 | func (a *apply) knownGVK() (ret []schema.GroupVersionKind) { 74 | for k := range a.pruneTypes { 75 | ret = append(ret, k) 76 | } 77 | return 78 | } 79 | 80 | func (a *apply) debugID() string { 81 | if a.owner == nil { 82 | return a.ownerSubContext 83 | } 84 | metadata, err := meta.Accessor(a.owner) 85 | if err != nil { 86 | return a.ownerSubContext 87 | } 88 | 89 | return fmt.Sprintf("subcontext [%s] namespace [%s] name [%s]", a.ownerSubContext, metadata.GetNamespace(), 90 | metadata.GetName()) 91 | } 92 | 93 | func GetSelector(labelSet map[string]string) (labels.Selector, error) { 94 | if len(labelSet) == 0 { 95 | return nil, nil 96 | } 97 | req, err := labels.NewRequirement(LabelHash, selection.Equals, []string{labelSet[LabelHash]}) 98 | if err != nil { 99 | return nil, err 100 | } 101 | return labels.NewSelector().Add(*req), nil 102 | } 103 | 104 | func GetLabelsAndAnnotations(scheme *runtime.Scheme, ownerSubContext string, owner kclient.Object) (map[string]string, map[string]string, error) { 105 | if ownerSubContext == "" && owner == nil { 106 | return nil, nil, nil 107 | } 108 | 109 | annotations := map[string]string{ 110 | LabelSubContext: ownerSubContext, 111 | } 112 | 113 | if ownerSubContext != "" { 114 | annotations[LabelSubContext] = ownerSubContext 115 | } 116 | 117 | if owner != nil { 118 | gvk, err := apiutil.GVKForObject(owner, scheme) 119 | if err != nil { 120 | return nil, nil, err 121 | } 122 | annotations[LabelGVK] = gvk.String() 123 | metadata, err := meta.Accessor(owner) 124 | if err != nil { 125 | return nil, nil, fmt.Errorf("failed to get metadata for %s", gvk) 126 | } 127 | annotations[LabelName] = metadata.GetName() 128 | annotations[LabelNamespace] = metadata.GetNamespace() 129 | } 130 | 131 | labels := map[string]string{ 132 | LabelHash: objectSetHash(annotations), 133 | } 134 | 135 | return labels, annotations, nil 136 | } 137 | 138 | func (a *apply) injectLabelsAndAnnotations(in *objectset.ObjectSet, labels, annotations map[string]string) (*objectset.ObjectSet, error) { 139 | result, err := objectset.NewObjectSet(a.client.Scheme()) 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | for _, objMap := range in.ObjectsByGVK() { 145 | for _, obj := range objMap { 146 | if !a.ensure { 147 | obj = obj.DeepCopyObject().(kclient.Object) 148 | } 149 | setLabels(obj, labels) 150 | setAnnotations(obj, annotations) 151 | if err := result.Add(obj); err != nil { 152 | return nil, err 153 | } 154 | } 155 | } 156 | 157 | return result, nil 158 | } 159 | 160 | func setAnnotations(meta kclient.Object, annotations map[string]string) { 161 | objAnn := meta.GetAnnotations() 162 | if objAnn == nil { 163 | objAnn = map[string]string{} 164 | } 165 | delete(objAnn, LabelApplied) 166 | for k, v := range annotations { 167 | objAnn[k] = v 168 | } 169 | meta.SetAnnotations(objAnn) 170 | } 171 | 172 | func setLabels(meta kclient.Object, labels map[string]string) { 173 | objLabels := meta.GetLabels() 174 | if objLabels == nil { 175 | objLabels = map[string]string{} 176 | } 177 | for k, v := range labels { 178 | objLabels[k] = v 179 | } 180 | meta.SetLabels(objLabels) 181 | } 182 | 183 | func objectSetHash(labels map[string]string) string { 184 | dig := sha1.New() 185 | for _, key := range hashOrder { 186 | dig.Write([]byte(labels[key])) 187 | } 188 | return hex.EncodeToString(dig.Sum(nil)) 189 | } 190 | -------------------------------------------------------------------------------- /pkg/apply/desiredset_crud.go: -------------------------------------------------------------------------------- 1 | package apply 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 5 | "k8s.io/apimachinery/pkg/runtime/schema" 6 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 7 | ) 8 | 9 | func (a *apply) create(gvk schema.GroupVersionKind, obj kclient.Object) (kclient.Object, error) { 10 | a.log("creating", gvk, obj) 11 | return obj, a.client.Create(a.ctx, obj) 12 | } 13 | 14 | func (a *apply) get(gvk schema.GroupVersionKind, obj kclient.Object, namespace, name string) (kclient.Object, error) { 15 | if obj == nil { 16 | newObj, err := a.newObj(gvk, false) 17 | if err != nil { 18 | return nil, err 19 | } 20 | obj = newObj.(kclient.Object) 21 | } else { 22 | obj = obj.DeepCopyObject().(kclient.Object) 23 | } 24 | 25 | return obj, a.client.Get(a.ctx, kclient.ObjectKey{Namespace: namespace, Name: name}, obj) 26 | } 27 | 28 | func (a *apply) delete(gvk schema.GroupVersionKind, namespace, name string) error { 29 | ustr := &unstructured.Unstructured{} 30 | ustr.SetGroupVersionKind(gvk) 31 | ustr.SetName(name) 32 | ustr.SetNamespace(namespace) 33 | a.log("deleting", gvk, ustr) 34 | return a.client.Delete(a.ctx, ustr) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/apply/desiredset_log.go: -------------------------------------------------------------------------------- 1 | package apply 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 9 | ) 10 | 11 | var ( 12 | LogInfo func(format string, args ...interface{}) 13 | ) 14 | 15 | func (a *apply) log(operation string, gvk schema.GroupVersionKind, obj kclient.Object) { 16 | if LogInfo == nil { 17 | return 18 | } 19 | if a.ensure { 20 | LogInfo("apply: %s [%s] [%s]", operation, logKey(obj), gvk) 21 | } else { 22 | LogInfo("apply: %s [%s] [%s] by owner %s", operation, logKey(obj), gvk, a.ownerLogKey()) 23 | } 24 | } 25 | 26 | func logKey(obj kclient.Object) string { 27 | ns, name := obj.GetNamespace(), obj.GetName() 28 | if ns == "" { 29 | return name 30 | } 31 | return ns + "/" + name 32 | } 33 | 34 | func (a *apply) ownerLogKey() string { 35 | var result strings.Builder 36 | if a.owner != nil { 37 | result.WriteString("[") 38 | result.WriteString(logKey(a.owner)) 39 | result.WriteString("] [") 40 | result.WriteString(fmt.Sprint(a.ownerGVK)) 41 | result.WriteString("]") 42 | } 43 | if a.ownerSubContext != "" { 44 | if result.Len() > 0 { 45 | result.WriteString(" ") 46 | } 47 | result.WriteString("subctx [") 48 | result.WriteString(a.ownerSubContext) 49 | result.WriteString("]") 50 | } 51 | return result.String() 52 | } 53 | -------------------------------------------------------------------------------- /pkg/apply/desiredset_owner.go: -------------------------------------------------------------------------------- 1 | package apply 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | 9 | apierrors "k8s.io/apimachinery/pkg/api/errors" 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 12 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 13 | ) 14 | 15 | var ( 16 | ErrOwnerNotFound = errors.New("owner not found") 17 | ) 18 | 19 | func getGVK(gvkLabel string, gvk *schema.GroupVersionKind) error { 20 | parts := strings.Split(gvkLabel, ", Kind=") 21 | if len(parts) != 2 { 22 | return fmt.Errorf("invalid GVK format: %s", gvkLabel) 23 | } 24 | gvk.Group, gvk.Version, _ = strings.Cut(parts[0], "/") 25 | gvk.Kind = parts[1] 26 | return nil 27 | } 28 | 29 | func (a apply) FindOwner(ctx context.Context, obj kclient.Object) (kclient.Object, error) { 30 | if obj == nil { 31 | return nil, ErrOwnerNotFound 32 | } 33 | 34 | a.ctx = ctx 35 | 36 | var ( 37 | gvkLabel = obj.GetAnnotations()[LabelGVK] 38 | namespace = obj.GetAnnotations()[LabelNamespace] 39 | name = obj.GetAnnotations()[LabelName] 40 | gvk schema.GroupVersionKind 41 | ) 42 | 43 | if gvkLabel == "" { 44 | return nil, ErrOwnerNotFound 45 | } 46 | 47 | if err := getGVK(gvkLabel, &gvk); err != nil { 48 | return nil, err 49 | } 50 | 51 | return a.get(gvk, nil, namespace, name) 52 | } 53 | 54 | func (a apply) PurgeOrphan(ctx context.Context, obj kclient.Object) error { 55 | if obj == nil { 56 | return nil 57 | } 58 | 59 | a.ctx = ctx 60 | 61 | if _, err := a.FindOwner(ctx, obj); apierrors.IsNotFound(err) { 62 | gvk, err := apiutil.GVKForObject(obj, a.client.Scheme()) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | return a.delete(gvk, obj.GetNamespace(), obj.GetName()) 68 | } else if err == ErrOwnerNotFound { 69 | return nil 70 | } else if err != nil { 71 | return err 72 | } 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /pkg/apply/objectset/objectset.go: -------------------------------------------------------------------------------- 1 | package objectset 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "sort" 7 | 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "k8s.io/apimachinery/pkg/runtime/schema" 10 | "k8s.io/apimachinery/pkg/util/sets" 11 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 12 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 13 | ) 14 | 15 | type ObjectKey struct { 16 | Name string 17 | Namespace string 18 | } 19 | 20 | func (o ObjectKey) String() string { 21 | if o.Namespace == "" { 22 | return o.Name 23 | } 24 | return fmt.Sprintf("%s/%s", o.Namespace, o.Name) 25 | } 26 | 27 | type ObjectKeyByGVK map[schema.GroupVersionKind][]ObjectKey 28 | 29 | type ObjectByGVK map[schema.GroupVersionKind]map[ObjectKey]kclient.Object 30 | 31 | func (o ObjectByGVK) Add(gvk schema.GroupVersionKind, obj kclient.Object) { 32 | objs := o[gvk] 33 | if objs == nil { 34 | objs = ObjectByKey{} 35 | o[gvk] = objs 36 | } 37 | 38 | objs[ObjectKey{ 39 | Namespace: obj.GetNamespace(), 40 | Name: obj.GetName(), 41 | }] = obj 42 | } 43 | 44 | type ObjectSet struct { 45 | scheme *runtime.Scheme 46 | objects ObjectByGVK 47 | objectsByGK ObjectByGK 48 | order []kclient.Object 49 | gvkOrder []schema.GroupVersionKind 50 | gvkSeen map[schema.GroupVersionKind]bool 51 | } 52 | 53 | func NewObjectSet(scheme *runtime.Scheme, objs ...kclient.Object) (*ObjectSet, error) { 54 | os := &ObjectSet{ 55 | scheme: scheme, 56 | objects: ObjectByGVK{}, 57 | objectsByGK: ObjectByGK{}, 58 | gvkSeen: map[schema.GroupVersionKind]bool{}, 59 | } 60 | return os, os.Add(objs...) 61 | } 62 | 63 | func (o *ObjectSet) ObjectsByGVK() ObjectByGVK { 64 | if o == nil { 65 | return nil 66 | } 67 | return o.objects 68 | } 69 | 70 | func (o *ObjectSet) Contains(gk schema.GroupKind, key ObjectKey) bool { 71 | _, ok := o.objectsByGK[gk][key] 72 | return ok 73 | } 74 | 75 | func (o *ObjectSet) All() []kclient.Object { 76 | return o.order 77 | } 78 | 79 | func (o *ObjectSet) Add(objs ...kclient.Object) error { 80 | for _, obj := range objs { 81 | if err := o.add(obj); err != nil { 82 | return err 83 | } 84 | } 85 | return nil 86 | } 87 | 88 | func (o *ObjectSet) add(obj kclient.Object) error { 89 | if obj == nil || reflect.ValueOf(obj).IsNil() { 90 | return nil 91 | } 92 | 93 | gvk, err := apiutil.GVKForObject(obj, o.scheme) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | o.objects.Add(gvk, obj) 99 | o.objectsByGK.Add(gvk, obj) 100 | 101 | o.order = append(o.order, obj) 102 | if !o.gvkSeen[gvk] { 103 | o.gvkSeen[gvk] = true 104 | o.gvkOrder = append(o.gvkOrder, gvk) 105 | } 106 | 107 | return nil 108 | } 109 | 110 | func (o *ObjectSet) Len() int { 111 | return len(o.objects) 112 | } 113 | 114 | func (o *ObjectSet) GVKs() []schema.GroupVersionKind { 115 | return o.GVKOrder() 116 | } 117 | 118 | func (o *ObjectSet) GVKOrder(known ...schema.GroupVersionKind) []schema.GroupVersionKind { 119 | var rest []schema.GroupVersionKind 120 | 121 | for _, gvk := range known { 122 | if o.gvkSeen[gvk] { 123 | continue 124 | } 125 | rest = append(rest, gvk) 126 | } 127 | 128 | sort.Slice(rest, func(i, j int) bool { 129 | return rest[i].String() < rest[j].String() 130 | }) 131 | 132 | return append(o.gvkOrder, rest...) 133 | } 134 | 135 | // Namespaces all distinct namespaces found on the objects in this set. 136 | func (o *ObjectSet) Namespaces() []string { 137 | namespaces := sets.New[string]() 138 | for _, objsByKey := range o.ObjectsByGVK() { 139 | for objKey := range objsByKey { 140 | namespaces.Insert(objKey.Namespace) 141 | } 142 | } 143 | return sets.List(namespaces) 144 | } 145 | 146 | type ObjectByKey map[ObjectKey]kclient.Object 147 | 148 | func (o ObjectByKey) Namespaces() []string { 149 | namespaces := sets.New[string]() 150 | for objKey := range o { 151 | namespaces.Insert(objKey.Namespace) 152 | } 153 | return sets.List(namespaces) 154 | } 155 | 156 | type ObjectByGK map[schema.GroupKind]map[ObjectKey]kclient.Object 157 | 158 | func (o ObjectByGK) Add(gvk schema.GroupVersionKind, obj kclient.Object) { 159 | gk := gvk.GroupKind() 160 | 161 | objs := o[gk] 162 | if objs == nil { 163 | objs = ObjectByKey{} 164 | o[gk] = objs 165 | } 166 | 167 | objs[ObjectKey{ 168 | Namespace: obj.GetNamespace(), 169 | Name: obj.GetName(), 170 | }] = obj 171 | } 172 | -------------------------------------------------------------------------------- /pkg/apply/patch_style.go: -------------------------------------------------------------------------------- 1 | package apply 2 | 3 | import ( 4 | "sync" 5 | 6 | "k8s.io/apimachinery/pkg/runtime" 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | "k8s.io/apimachinery/pkg/types" 9 | "k8s.io/apimachinery/pkg/util/strategicpatch" 10 | "k8s.io/client-go/kubernetes/scheme" 11 | ) 12 | 13 | var ( 14 | patchCache = map[schema.GroupVersionKind]patchCacheEntry{} 15 | patchCacheLock = sync.Mutex{} 16 | ) 17 | 18 | type patchCacheEntry struct { 19 | patchType types.PatchType 20 | lookup strategicpatch.LookupPatchMeta 21 | } 22 | 23 | func getMergeStyle(gvk schema.GroupVersionKind) (types.PatchType, strategicpatch.LookupPatchMeta, error) { 24 | var ( 25 | patchType types.PatchType 26 | lookupPatchMeta strategicpatch.LookupPatchMeta 27 | ) 28 | 29 | patchCacheLock.Lock() 30 | entry, ok := patchCache[gvk] 31 | patchCacheLock.Unlock() 32 | 33 | if ok { 34 | return entry.patchType, entry.lookup, nil 35 | } 36 | 37 | versionedObject, err := scheme.Scheme.New(gvk) 38 | 39 | if runtime.IsNotRegisteredError(err) || gvk.Kind == "CustomResourceDefinition" { 40 | patchType = types.MergePatchType 41 | } else if err != nil { 42 | return patchType, nil, err 43 | } else { 44 | patchType = types.StrategicMergePatchType 45 | lookupPatchMeta, err = strategicpatch.NewPatchMetaFromStruct(versionedObject) 46 | if err != nil { 47 | return patchType, nil, err 48 | } 49 | } 50 | 51 | patchCacheLock.Lock() 52 | patchCache[gvk] = patchCacheEntry{ 53 | patchType: patchType, 54 | lookup: lookupPatchMeta, 55 | } 56 | patchCacheLock.Unlock() 57 | 58 | return patchType, lookupPatchMeta, nil 59 | } 60 | -------------------------------------------------------------------------------- /pkg/apply/reconcilers.go: -------------------------------------------------------------------------------- 1 | package apply 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | 8 | appsv1 "k8s.io/api/apps/v1" 9 | batchv1 "k8s.io/api/batch/v1" 10 | v1 "k8s.io/api/core/v1" 11 | "k8s.io/apimachinery/pkg/api/equality" 12 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 13 | "k8s.io/apimachinery/pkg/runtime/schema" 14 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 15 | ) 16 | 17 | var ( 18 | defaultReconcilers = map[schema.GroupVersionKind]reconciler{ 19 | v1.SchemeGroupVersion.WithKind("Secret"): reconcileSecret, 20 | v1.SchemeGroupVersion.WithKind("Service"): reconcileService, 21 | batchv1.SchemeGroupVersion.WithKind("Job"): reconcileJob, 22 | appsv1.SchemeGroupVersion.WithKind("Deployment"): reconcileDeployment, 23 | appsv1.SchemeGroupVersion.WithKind("DaemonSet"): reconcileDaemonSet, 24 | } 25 | ) 26 | 27 | func reconcileDaemonSet(oldObj, newObj kclient.Object) (bool, error) { 28 | oldSvc, ok := oldObj.(*appsv1.DaemonSet) 29 | if !ok { 30 | oldSvc = &appsv1.DaemonSet{} 31 | if err := convertObj(oldObj, oldSvc); err != nil { 32 | return false, err 33 | } 34 | } 35 | newSvc, ok := newObj.(*appsv1.DaemonSet) 36 | if !ok { 37 | newSvc = &appsv1.DaemonSet{} 38 | if err := convertObj(newObj, newSvc); err != nil { 39 | return false, err 40 | } 41 | } 42 | 43 | if !equality.Semantic.DeepEqual(oldSvc.Spec.Selector, newSvc.Spec.Selector) { 44 | return false, ErrReplace 45 | } 46 | 47 | return false, nil 48 | } 49 | 50 | func reconcileDeployment(oldObj, newObj kclient.Object) (bool, error) { 51 | oldSvc, ok := oldObj.(*appsv1.Deployment) 52 | if !ok { 53 | oldSvc = &appsv1.Deployment{} 54 | if err := convertObj(oldObj, oldSvc); err != nil { 55 | return false, err 56 | } 57 | } 58 | newSvc, ok := newObj.(*appsv1.Deployment) 59 | if !ok { 60 | newSvc = &appsv1.Deployment{} 61 | if err := convertObj(newObj, newSvc); err != nil { 62 | return false, err 63 | } 64 | } 65 | 66 | if !equality.Semantic.DeepEqual(oldSvc.Spec.Selector, newSvc.Spec.Selector) { 67 | return false, ErrReplace 68 | } 69 | 70 | return false, nil 71 | } 72 | 73 | func reconcileSecret(oldObj, newObj kclient.Object) (bool, error) { 74 | oldSvc, ok := oldObj.(*v1.Secret) 75 | if !ok { 76 | oldSvc = &v1.Secret{} 77 | if err := convertObj(oldObj, oldSvc); err != nil { 78 | return false, err 79 | } 80 | } 81 | newSvc, ok := newObj.(*v1.Secret) 82 | if !ok { 83 | newSvc = &v1.Secret{} 84 | if err := convertObj(newObj, newSvc); err != nil { 85 | return false, err 86 | } 87 | } 88 | 89 | if newSvc.Type != "" && oldSvc.Type != newSvc.Type { 90 | return false, ErrReplace 91 | } 92 | 93 | return false, nil 94 | } 95 | 96 | func reconcileService(oldObj, newObj kclient.Object) (bool, error) { 97 | oldSvc, ok := oldObj.(*v1.Service) 98 | if !ok { 99 | oldSvc = &v1.Service{} 100 | if err := convertObj(oldObj, oldSvc); err != nil { 101 | return false, err 102 | } 103 | } 104 | newSvc, ok := newObj.(*v1.Service) 105 | if !ok { 106 | newSvc = &v1.Service{} 107 | if err := convertObj(newObj, newSvc); err != nil { 108 | return false, err 109 | } 110 | } 111 | 112 | if newSvc.Spec.Type != "" && oldSvc.Spec.Type != newSvc.Spec.Type { 113 | return false, ErrReplace 114 | } 115 | 116 | return false, nil 117 | } 118 | 119 | func reconcileJob(oldObj, newObj kclient.Object) (bool, error) { 120 | oldJob, ok := oldObj.(*batchv1.Job) 121 | if !ok { 122 | oldJob = &batchv1.Job{} 123 | if err := convertObj(oldObj, oldJob); err != nil { 124 | return false, err 125 | } 126 | } 127 | 128 | newJob, ok := newObj.(*batchv1.Job) 129 | if !ok { 130 | newJob = &batchv1.Job{} 131 | if err := convertObj(newObj, newJob); err != nil { 132 | return false, err 133 | } 134 | } 135 | 136 | // We round trip the object here because when serializing to the applied 137 | // annotation values are truncated to 64 bytes. 138 | prunedSvc, err := getOriginalObject(newJob.GroupVersionKind(), newJob) 139 | if err != nil { 140 | return false, err 141 | } 142 | 143 | newPrunedJob := &batchv1.Job{} 144 | if err := convertObj(prunedSvc, newPrunedJob); err != nil { 145 | return false, err 146 | } 147 | 148 | if !equality.Semantic.DeepEqual(oldJob.Spec.Template, newPrunedJob.Spec.Template) { 149 | return false, ErrReplace 150 | } 151 | 152 | return false, nil 153 | } 154 | 155 | func convertObj(src interface{}, obj interface{}) error { 156 | uObj, ok := src.(*unstructured.Unstructured) 157 | if !ok { 158 | return fmt.Errorf("expected unstructured but got %v", reflect.TypeOf(src)) 159 | } 160 | 161 | bytes, err := uObj.MarshalJSON() 162 | if err != nil { 163 | return err 164 | } 165 | return json.Unmarshal(bytes, obj) 166 | } 167 | -------------------------------------------------------------------------------- /pkg/backend/backend.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "k8s.io/apimachinery/pkg/runtime" 8 | "k8s.io/apimachinery/pkg/runtime/schema" 9 | "k8s.io/client-go/tools/cache" 10 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 11 | ) 12 | 13 | type Callback func(gvk schema.GroupVersionKind, key string, obj runtime.Object) (runtime.Object, error) 14 | 15 | type Trigger interface { 16 | Trigger(gvk schema.GroupVersionKind, key string, delay time.Duration) error 17 | } 18 | 19 | type Watcher interface { 20 | Watcher(ctx context.Context, gvk schema.GroupVersionKind, name string, cb Callback) error 21 | } 22 | 23 | type Backend interface { 24 | Trigger 25 | CacheFactory 26 | Watcher 27 | kclient.WithWatch 28 | kclient.FieldIndexer 29 | 30 | Start(ctx context.Context) error 31 | GVKForObject(obj runtime.Object, scheme *runtime.Scheme) (schema.GroupVersionKind, error) 32 | } 33 | 34 | type CacheFactory interface { 35 | GetInformerForKind(ctx context.Context, gvk schema.GroupVersionKind) (cache.SharedIndexInformer, error) 36 | } 37 | -------------------------------------------------------------------------------- /pkg/conditions/setter.go: -------------------------------------------------------------------------------- 1 | package conditions 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log/slog" 7 | "reflect" 8 | 9 | "github.com/acorn-io/baaah/pkg/router" 10 | apierrors "k8s.io/apimachinery/pkg/api/errors" 11 | "k8s.io/apimachinery/pkg/api/meta" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | ) 14 | 15 | type Conditions interface { 16 | GetConditions() *[]metav1.Condition 17 | } 18 | 19 | // ErrTerminal is an error that can not be recovered from until some other state in the 20 | // system changes, typically from additional user input. 21 | type ErrTerminal struct { 22 | Cause error 23 | } 24 | 25 | func NewErrTerminal(cause error) *ErrTerminal { 26 | return &ErrTerminal{ 27 | Cause: cause, 28 | } 29 | } 30 | 31 | func NewErrTerminalf(format string, args ...any) *ErrTerminal { 32 | return &ErrTerminal{ 33 | Cause: fmt.Errorf(format, args...), 34 | } 35 | } 36 | 37 | func (e *ErrTerminal) Error() string { 38 | return e.Cause.Error() 39 | } 40 | 41 | func (e *ErrTerminal) Unwrap() error { 42 | return e.Cause 43 | } 44 | 45 | func ErrorMiddleware() router.Middleware { 46 | return func(h router.Handler) router.Handler { 47 | return router.HandlerFunc(func(req router.Request, resp router.Response) error { 48 | var ( 49 | uErr *ErrTerminal 50 | logErr error 51 | ) 52 | 53 | t, ok := req.Object.(Conditions) 54 | if !ok { 55 | return h.Handle(req, resp) 56 | } 57 | 58 | err := h.Handle(req, resp) 59 | if errors.As(err, &uErr) { 60 | logErr = uErr 61 | } else if apierrors.IsNotFound(err) { 62 | logErr = err 63 | } 64 | 65 | existing, ok := resp.Attributes()["_errormiddleware"].(*metav1.Condition) 66 | if !ok { 67 | existing = meta.FindStatusCondition(*t.GetConditions(), "Controller") 68 | if existing != nil { 69 | cp := *existing 70 | resp.Attributes()["_errormiddleware"] = &cp 71 | } 72 | } 73 | 74 | errored, _ := resp.Attributes()["_errormiddleware:errored"].(bool) 75 | if errored { 76 | if logErr != nil { 77 | return nil 78 | } 79 | return err 80 | } 81 | 82 | if logErr != nil { 83 | slog.Error("error processing controller", 84 | "err", logErr, 85 | "gvk", req.GVK, 86 | "namespace", req.Namespace, 87 | "name", req.Name) 88 | if existing != nil { 89 | meta.SetStatusCondition(t.GetConditions(), *existing) 90 | } 91 | meta.SetStatusCondition(t.GetConditions(), metav1.Condition{ 92 | Type: "Controller", 93 | Status: metav1.ConditionFalse, 94 | ObservedGeneration: req.Object.GetGeneration(), 95 | Reason: reflect.TypeOf(logErr).Name(), 96 | Message: logErr.Error(), 97 | }) 98 | resp.Attributes()["_errormiddleware:errored"] = true 99 | resp.DisablePrune() 100 | return nil 101 | } 102 | 103 | if err == nil { 104 | if existing != nil { 105 | meta.SetStatusCondition(t.GetConditions(), *existing) 106 | } 107 | meta.SetStatusCondition(t.GetConditions(), metav1.Condition{ 108 | Type: "Controller", 109 | Status: metav1.ConditionTrue, 110 | ObservedGeneration: req.Object.GetGeneration(), 111 | }) 112 | return nil 113 | } else { 114 | return err 115 | } 116 | }) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /pkg/data/generic.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/acorn-io/baaah/pkg/typed" 9 | "github.com/sirupsen/logrus" 10 | "k8s.io/kube-openapi/pkg/common" 11 | "k8s.io/kube-openapi/pkg/validation/spec" 12 | ) 13 | 14 | type GenericMap struct { 15 | // +optional 16 | Data map[string]any `json:"-"` 17 | } 18 | 19 | func (GenericMap) OpenAPIDefinition() common.OpenAPIDefinition { 20 | return common.OpenAPIDefinition{ 21 | Schema: spec.Schema{ 22 | VendorExtensible: spec.VendorExtensible{ 23 | Extensions: spec.Extensions{ 24 | "x-kubernetes-preserve-unknown-fields": "true", 25 | }, 26 | }, 27 | SchemaProps: spec.SchemaProps{ 28 | Type: []string{"object"}, 29 | }, 30 | }, 31 | } 32 | } 33 | 34 | func (g *GenericMap) UnmarshalJSON(data []byte) error { 35 | if g == nil { 36 | return fmt.Errorf("%T: UnmarshalJSON on nil pointer", g) 37 | } 38 | 39 | dec := json.NewDecoder(bytes.NewBuffer(data)) 40 | dec.UseNumber() 41 | 42 | d := map[string]any{} 43 | if err := dec.Decode(&d); err != nil { 44 | return fmt.Errorf("%T: Failed to decode data: %w", g, err) 45 | } 46 | 47 | if _, err := translateObject(d); err != nil { 48 | return fmt.Errorf("%T: Failed to translate object: %w", g, err) 49 | } 50 | 51 | if len(d) > 0 { 52 | // Consumers expect empty generic maps to have a nil Data field 53 | g.Data = d 54 | } 55 | 56 | return nil 57 | } 58 | 59 | // GetData returns the underlying map[string]any and nil if the GenericMap is nil. 60 | func (g *GenericMap) GetData() map[string]any { 61 | if g == nil { 62 | return nil 63 | } 64 | 65 | return g.Data 66 | } 67 | 68 | // Merge merges the given map into this map, returning a new map, leaving the original unchanged. 69 | func (g *GenericMap) Merge(from *GenericMap) *GenericMap { 70 | merged := typed.Concat(g.GetData(), from.GetData()) 71 | if merged == nil { 72 | return nil 73 | } 74 | 75 | return &GenericMap{ 76 | Data: merged, 77 | } 78 | } 79 | 80 | // MarshalJSON may get called on pointers or values, so implement MarshalJSON on value. 81 | func (g *GenericMap) MarshalJSON() ([]byte, error) { 82 | return json.Marshal(g.GetData()) 83 | } 84 | 85 | func translateObject(data any) (ret any, err error) { 86 | switch t := data.(type) { 87 | case map[string]any: 88 | for k, v := range t { 89 | if t[k], err = translateObject(v); err != nil { 90 | return nil, err 91 | } 92 | } 93 | case []any: 94 | for i, v := range t { 95 | if t[i], err = translateObject(v); err != nil { 96 | return nil, err 97 | } 98 | } 99 | case json.Number: 100 | i, err := t.Int64() 101 | if err == nil { 102 | return i, nil 103 | } 104 | return t.Float64() 105 | } 106 | return data, nil 107 | } 108 | 109 | func (in *GenericMap) DeepCopyInto(out *GenericMap) { 110 | var err error 111 | if *out, err = Mapify(in.GetData()); err != nil { 112 | logrus.WithError(err).Errorf("failed to deep copy into [%T]", out) 113 | } 114 | } 115 | 116 | func (in GenericMap) DeepCopy() GenericMap { 117 | var out GenericMap 118 | in.DeepCopyInto(&out) 119 | return out 120 | } 121 | 122 | func NewGenericMap(data map[string]any) *GenericMap { 123 | if data == nil { 124 | return nil 125 | } 126 | 127 | return &GenericMap{ 128 | Data: data, 129 | } 130 | } 131 | 132 | func Mapify(data any) (GenericMap, error) { 133 | marshaled, err := json.Marshal(data) 134 | if err != nil { 135 | return GenericMap{}, err 136 | } 137 | 138 | gm := &GenericMap{} 139 | if err := gm.UnmarshalJSON(marshaled); err != nil { 140 | return GenericMap{}, err 141 | } 142 | 143 | return *gm, nil 144 | } 145 | -------------------------------------------------------------------------------- /pkg/data/map.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | 7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 8 | ) 9 | 10 | func ToMapInterface(obj interface{}) (map[string]interface{}, error) { 11 | if m, ok := obj.(map[string]interface{}); ok { 12 | return m, nil 13 | } 14 | 15 | if unstr, ok := obj.(*unstructured.Unstructured); ok { 16 | return unstr.Object, nil 17 | } 18 | 19 | b, err := json.Marshal(obj) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | result := map[string]interface{}{} 25 | dec := json.NewDecoder(bytes.NewBuffer(b)) 26 | dec.UseNumber() 27 | return result, dec.Decode(&result) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/deepcopy/deepcopy.go: -------------------------------------------------------------------------------- 1 | package deepcopy 2 | 3 | import ( 4 | "sigs.k8s.io/controller-tools/pkg/deepcopy" 5 | "sigs.k8s.io/controller-tools/pkg/genall" 6 | ) 7 | 8 | func Deepcopy(paths ...string) { 9 | var deepcopy genall.Generator = deepcopy.Generator{} 10 | for _, dir := range paths { 11 | runtime, err := genall.Generators{&deepcopy}.ForRoots(dir) 12 | if err != nil { 13 | panic(err) 14 | } 15 | runtime.OutputRules.Default = genall.OutputToDirectory(dir) 16 | runtime.Run() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pkg/fields/fields.go: -------------------------------------------------------------------------------- 1 | package fields 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/fields" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 8 | ) 9 | 10 | type Fields interface { 11 | fields.Fields 12 | FieldNames() []string 13 | } 14 | 15 | func AddFieldConversion(scheme *runtime.Scheme, obj runtime.Object) error { 16 | gvk, err := apiutil.GVKForObject(obj, scheme) 17 | if err != nil { 18 | // ignore errors in determining GVK 19 | return nil 20 | } 21 | f, ok := obj.(Fields) 22 | if !ok { 23 | return nil 24 | } 25 | return scheme.AddFieldLabelConversionFunc(gvk, ValidSelectors(f.FieldNames()...)) 26 | } 27 | 28 | func AddKnownTypesWithFieldConversion(scheme *runtime.Scheme, gv schema.GroupVersion, types ...runtime.Object) error { 29 | for _, obj := range types { 30 | scheme.AddKnownTypes(gv, obj) 31 | if err := AddFieldConversion(scheme, obj); err != nil { 32 | return err 33 | } 34 | } 35 | return nil 36 | } 37 | 38 | func ValidSelectors(labels ...string) func(string, string) (string, string, error) { 39 | return func(label string, value string) (string, string, error) { 40 | for _, checkLabel := range labels { 41 | if label == checkLabel { 42 | return label, value, nil 43 | } 44 | } 45 | return runtime.DefaultMetaV1FieldSelectorConversion(label, value) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pkg/handlers/handlers.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/acorn-io/baaah/pkg/apply" 5 | "github.com/acorn-io/baaah/pkg/router" 6 | ) 7 | 8 | // GCOrphans will delete an object whose owner has been deleted. 9 | func GCOrphans(req router.Request, _ router.Response) error { 10 | return apply.New(req.Client).PurgeOrphan(req.Ctx, req.Object) 11 | } 12 | 13 | // DoNothing is a handler that does nothing. It is useful because resp.Objects will only work if a GVK is being watched. 14 | // This ensures that resp.Objects does what it is supposed to for objects that are no longer passed to resp.Objects, and 15 | // are not watched anywhere else. 16 | func DoNothing(router.Request, router.Response) error { 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /pkg/leader/leader.go: -------------------------------------------------------------------------------- 1 | package leader 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/acorn-io/baaah/pkg/log" 12 | "k8s.io/client-go/rest" 13 | "k8s.io/client-go/tools/leaderelection" 14 | "k8s.io/client-go/tools/leaderelection/resourcelock" 15 | ) 16 | 17 | const ( 18 | defaultLeaderTTL = time.Minute 19 | devLeaderTTL = time.Hour 20 | ) 21 | 22 | type OnLeader func(context.Context) error 23 | type OnNewLeader func(string) 24 | 25 | type ElectionConfig struct { 26 | TTL time.Duration 27 | Name, Namespace, ResourceLockType string 28 | restCfg *rest.Config 29 | } 30 | 31 | func NewDefaultElectionConfig(namespace, name string, cfg *rest.Config) *ElectionConfig { 32 | ttl := defaultLeaderTTL 33 | if os.Getenv("BAAAH_DEV_MODE") != "" { 34 | ttl = devLeaderTTL 35 | } 36 | return &ElectionConfig{ 37 | TTL: ttl, 38 | Namespace: namespace, 39 | Name: name, 40 | ResourceLockType: resourcelock.LeasesResourceLock, 41 | restCfg: cfg, 42 | } 43 | } 44 | 45 | func NewElectionConfig(ttl time.Duration, namespace, name, lockType string, cfg *rest.Config) *ElectionConfig { 46 | return &ElectionConfig{ 47 | TTL: ttl, 48 | Namespace: namespace, 49 | Name: name, 50 | ResourceLockType: lockType, 51 | restCfg: cfg, 52 | } 53 | } 54 | 55 | func (ec *ElectionConfig) Run(ctx context.Context, id string, onLeader OnLeader, onSwitchLeader OnNewLeader) error { 56 | if ec == nil { 57 | // Don't start leader election if there is no config. 58 | return onLeader(ctx) 59 | } 60 | 61 | if ec.Namespace == "" { 62 | ec.Namespace = "kube-system" 63 | } 64 | 65 | if err := ec.run(ctx, id, onLeader, onSwitchLeader); err != nil { 66 | return fmt.Errorf("failed to start leader election for %s: %v", ec.Name, err) 67 | } 68 | 69 | return nil 70 | } 71 | 72 | func (ec *ElectionConfig) run(ctx context.Context, id string, cb OnLeader, onSwitchLeader OnNewLeader) error { 73 | rl, err := resourcelock.NewFromKubeconfig( 74 | ec.ResourceLockType, 75 | ec.Namespace, 76 | ec.Name, 77 | resourcelock.ResourceLockConfig{ 78 | Identity: id, 79 | }, 80 | ec.restCfg, 81 | ec.TTL/2, 82 | ) 83 | if err != nil { 84 | return fmt.Errorf("error creating leader lock for %s: %v", ec.Name, err) 85 | } 86 | 87 | // Catch these signals to ensure a graceful shutdown and leader election release. 88 | sigCtx, cancel := signal.NotifyContext(ctx, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGKILL) 89 | defer func() { 90 | if err != nil { 91 | // If we encountered an error, cancel the context because we won't be using it. 92 | cancel() 93 | } 94 | }() 95 | 96 | le, err := leaderelection.NewLeaderElector(leaderelection.LeaderElectionConfig{ 97 | Lock: rl, 98 | LeaseDuration: ec.TTL, 99 | RenewDeadline: ec.TTL / 2, 100 | RetryPeriod: ec.TTL / 4, 101 | Callbacks: leaderelection.LeaderCallbacks{ 102 | OnStartedLeading: func(ctx context.Context) { 103 | if err := cb(ctx); err != nil { 104 | log.Fatalf("leader callback error: %v", err) 105 | } 106 | }, 107 | OnNewLeader: onSwitchLeader, 108 | OnStoppedLeading: func() { 109 | select { 110 | case <-sigCtx.Done(): 111 | // Must cancel so that the registered signals are no longer caught. 112 | cancel() 113 | 114 | // The context has been canceled or is otherwise complete. 115 | // This is a request to terminate. Exit 0. 116 | // Exiting cleanly is useful when the context is canceled 117 | // so that Kubernetes doesn't record it exiting in error 118 | // when the exit was requested. For example, the wrangler-cli 119 | // package sets up a context that cancels when SIGTERM is 120 | // sent in. If a node is shut down this is the type of signal 121 | // sent. In that case you want the 0 exit code to mark it as 122 | // complete so that everything comes back up correctly after 123 | // a restart. 124 | // The pattern found here can be found inside the kube-scheduler. 125 | log.Infof("requested to terminate, exiting") 126 | os.Exit(0) 127 | default: 128 | log.Fatalf("leader election lost for %s", ec.Name) 129 | } 130 | }, 131 | }, 132 | ReleaseOnCancel: true, 133 | }) 134 | if err != nil { 135 | return err 136 | } 137 | 138 | go func() { 139 | le.Run(sigCtx) 140 | }() 141 | return nil 142 | } 143 | -------------------------------------------------------------------------------- /pkg/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | var ( 8 | // Stupid log abstraction. It's expected that the consumer 9 | // of this library override these methods like 10 | // 11 | // log.Infof = logrus.Infof 12 | // log.Errorf = logrus.Errorf 13 | // 14 | // or you can call SetLogger 15 | 16 | Infof = func(message string, obj ...interface{}) { 17 | //log.Printf("INFO: "+message+"\n", obj...) 18 | } 19 | Warnf = func(message string, obj ...interface{}) { 20 | log.Printf("WARN [BAAAH]: "+message+"\n", obj...) 21 | } 22 | Errorf = func(message string, obj ...interface{}) { 23 | log.Printf("ERROR[BAAAH]: "+message+"\n", obj...) 24 | } 25 | Fatalf = func(message string, obj ...interface{}) { 26 | log.Fatalf("FATAL[BAAAH]: "+message+"\n", obj...) 27 | } 28 | Debugf = func(message string, obj ...interface{}) { 29 | //log.Printf("DEBUG: "+message+"\n", obj...) 30 | } 31 | ) 32 | 33 | type Logger interface { 34 | Infof(message string, obj ...interface{}) 35 | Warnf(message string, obj ...interface{}) 36 | Errorf(message string, obj ...interface{}) 37 | Fatalf(message string, obj ...interface{}) 38 | Debugf(message string, obj ...interface{}) 39 | } 40 | 41 | func SetLogger(logger Logger) { 42 | Debugf = logger.Debugf 43 | Infof = logger.Infof 44 | Warnf = logger.Warnf 45 | Errorf = logger.Errorf 46 | Fatalf = logger.Fatalf 47 | } 48 | -------------------------------------------------------------------------------- /pkg/logrus/init.go: -------------------------------------------------------------------------------- 1 | package logrus 2 | 3 | import ( 4 | "github.com/acorn-io/baaah/pkg/log" 5 | "github.com/bombsimon/logrusr/v4" 6 | "github.com/sirupsen/logrus" 7 | klogv2 "k8s.io/klog/v2" 8 | crlog "sigs.k8s.io/controller-runtime/pkg/log" 9 | ) 10 | 11 | func init() { 12 | l := logrusr.New(logrus.StandardLogger()) 13 | klogv2.SetLogger(l) 14 | crlog.SetLogger(l) 15 | log.SetLogger(logrus.StandardLogger()) 16 | } 17 | -------------------------------------------------------------------------------- /pkg/mapper/mapper.go: -------------------------------------------------------------------------------- 1 | package mapper 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | 7 | "github.com/sirupsen/logrus" 8 | "k8s.io/apimachinery/pkg/api/meta" 9 | "k8s.io/apimachinery/pkg/runtime/schema" 10 | "k8s.io/client-go/discovery" 11 | "k8s.io/client-go/rest" 12 | "k8s.io/client-go/restmapper" 13 | ) 14 | 15 | var ( 16 | gvrCache = map[schema.GroupVersionResource][]schema.GroupVersionKind{} 17 | gvrGvrCache = map[schema.GroupVersionResource][]schema.GroupVersionResource{} 18 | gkCache = map[gkKey][]*meta.RESTMapping{} 19 | nameCache = map[string]string{} 20 | cacheLock sync.RWMutex 21 | ) 22 | 23 | type gkKey struct { 24 | versions string 25 | gk schema.GroupKind 26 | } 27 | 28 | func New(cfg *rest.Config) (meta.RESTMapper, error) { 29 | disc, err := discovery.NewDiscoveryClientForConfig(cfg) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | return &RESTMapperGlobalCache{ 35 | disc: disc, 36 | }, nil 37 | } 38 | 39 | type RESTMapperGlobalCache struct { 40 | disc discovery.DiscoveryInterface 41 | api []*restmapper.APIGroupResources 42 | } 43 | 44 | func (m *RESTMapperGlobalCache) withClient(f func(m meta.RESTMapper) error) error { 45 | cacheLock.Lock() 46 | defer cacheLock.Unlock() 47 | 48 | shouldRetry := true 49 | if len(m.api) == 0 { 50 | shouldRetry = false 51 | api, err := restmapper.GetAPIGroupResources(m.disc) 52 | if err != nil { 53 | return err 54 | } 55 | m.api = api 56 | } 57 | 58 | mapper := restmapper.NewDiscoveryRESTMapper(m.api) 59 | if err := f(mapper); err == nil { 60 | return nil 61 | } else { 62 | if !shouldRetry { 63 | return err 64 | } 65 | } 66 | 67 | api, err := restmapper.GetAPIGroupResources(m.disc) 68 | if err != nil { 69 | return err 70 | } 71 | m.api = api 72 | 73 | return f(restmapper.NewDiscoveryRESTMapper(m.api)) 74 | } 75 | 76 | func (m *RESTMapperGlobalCache) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) { 77 | gvk, err := m.KindsFor(resource) 78 | if err != nil { 79 | return schema.GroupVersionKind{}, nil 80 | } 81 | return gvk[0], nil 82 | } 83 | 84 | func (m *RESTMapperGlobalCache) KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error) { 85 | cacheLock.RLock() 86 | gvks, ok := gvrCache[resource] 87 | cacheLock.RUnlock() 88 | 89 | if ok { 90 | return gvks, nil 91 | } 92 | 93 | logrus.Debugf("RESTMapperGlobalCache cache miss for %v", resource) 94 | 95 | var err error 96 | err = m.withClient(func(m meta.RESTMapper) error { 97 | gvks, err = m.KindsFor(resource) 98 | return err 99 | }) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | cacheLock.Lock() 105 | gvrCache[resource] = gvks 106 | cacheLock.Unlock() 107 | 108 | return gvks, nil 109 | } 110 | 111 | func (m *RESTMapperGlobalCache) ResourceFor(input schema.GroupVersionResource) (schema.GroupVersionResource, error) { 112 | gvrs, err := m.ResourcesFor(input) 113 | if err != nil { 114 | return schema.GroupVersionResource{}, err 115 | } 116 | return gvrs[0], nil 117 | } 118 | 119 | func (m *RESTMapperGlobalCache) ResourcesFor(input schema.GroupVersionResource) ([]schema.GroupVersionResource, error) { 120 | cacheLock.RLock() 121 | gvrs, ok := gvrGvrCache[input] 122 | cacheLock.RUnlock() 123 | 124 | if ok { 125 | return gvrs, nil 126 | } 127 | 128 | logrus.Debugf("RESTMapperGlobalCache cache miss for %v", input) 129 | 130 | var err error 131 | err = m.withClient(func(m meta.RESTMapper) error { 132 | gvrs, err = m.ResourcesFor(input) 133 | return err 134 | }) 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | cacheLock.Lock() 140 | gvrGvrCache[input] = gvrs 141 | cacheLock.Unlock() 142 | 143 | return gvrs, nil 144 | } 145 | 146 | func (m *RESTMapperGlobalCache) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) { 147 | mappings, err := m.RESTMappings(gk, versions...) 148 | if err != nil { 149 | return nil, err 150 | } 151 | return mappings[0], nil 152 | } 153 | 154 | func (m *RESTMapperGlobalCache) RESTMappings(gk schema.GroupKind, versions ...string) ([]*meta.RESTMapping, error) { 155 | key := gkKey{ 156 | versions: strings.Join(versions, "|"), 157 | gk: gk, 158 | } 159 | cacheLock.RLock() 160 | mappings, ok := gkCache[key] 161 | cacheLock.RUnlock() 162 | 163 | if ok { 164 | return mappings, nil 165 | } 166 | 167 | logrus.Debugf("RESTMapperGlobalCache cache miss for %v, %v", gk, versions) 168 | 169 | var err error 170 | err = m.withClient(func(m meta.RESTMapper) error { 171 | mappings, err = m.RESTMappings(gk) 172 | return err 173 | }) 174 | if err != nil { 175 | return nil, err 176 | } 177 | 178 | cacheLock.Lock() 179 | gkCache[key] = mappings 180 | cacheLock.Unlock() 181 | 182 | return mappings, nil 183 | } 184 | 185 | func (m *RESTMapperGlobalCache) ResourceSingularizer(resource string) (string, error) { 186 | cacheLock.RLock() 187 | singular, ok := nameCache[resource] 188 | cacheLock.RUnlock() 189 | 190 | if ok { 191 | return singular, nil 192 | } 193 | 194 | logrus.Debugf("RESTMapperGlobalCache cache miss for %s", resource) 195 | 196 | var err error 197 | err = m.withClient(func(m meta.RESTMapper) error { 198 | singular, err = m.ResourceSingularizer(resource) 199 | return err 200 | }) 201 | if err != nil { 202 | return "", err 203 | } 204 | 205 | cacheLock.Lock() 206 | nameCache[resource] = singular 207 | cacheLock.Unlock() 208 | 209 | return singular, nil 210 | } 211 | -------------------------------------------------------------------------------- /pkg/merr/error.go: -------------------------------------------------------------------------------- 1 | package merr 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | type Errors []error 8 | 9 | func (e Errors) Err() error { 10 | return NewErrors(e...) 11 | } 12 | 13 | func (e Errors) Unwrap() []error { 14 | return e 15 | } 16 | 17 | func (e Errors) Error() string { 18 | buf := &strings.Builder{} 19 | for _, err := range e { 20 | if buf.Len() > 0 { 21 | buf.WriteString(", ") 22 | } 23 | buf.WriteString(err.Error()) 24 | } 25 | 26 | return buf.String() 27 | } 28 | 29 | func NewErrors(inErrors ...error) error { 30 | var errors []error 31 | for _, err := range inErrors { 32 | if err != nil { 33 | errors = append(errors, err) 34 | } 35 | } 36 | 37 | if len(errors) == 0 { 38 | return nil 39 | } else if len(errors) == 1 { 40 | return errors[0] 41 | } 42 | return Errors(errors) 43 | } 44 | -------------------------------------------------------------------------------- /pkg/name/name.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "strings" 7 | ) 8 | 9 | func SafeConcatNameWithSeparatorAndLength(length int, sep string, name ...string) string { 10 | fullPath := strings.Join(name, sep) 11 | if len(fullPath) < length { 12 | return fullPath 13 | } 14 | digest := sha256.Sum256([]byte(fullPath)) 15 | // since we cut the string in the middle, the last char may not be compatible with what is expected in k8s 16 | // we are checking and if necessary removing the last char 17 | c := fullPath[length-8] 18 | if 'a' <= c && c <= 'z' || '0' <= c && c <= '9' { 19 | return fullPath[0:length-7] + sep + hex.EncodeToString(digest[0:])[0:5] 20 | } 21 | 22 | return fullPath[0:length-8] + sep + hex.EncodeToString(digest[0:])[0:6] 23 | } 24 | 25 | func SafeConcatName(name ...string) string { 26 | return SafeConcatNameWithSeparatorAndLength(64, "-", name...) 27 | } 28 | 29 | func SafeHashConcatName(name ...string) string { 30 | d := sha256.New() 31 | for _, part := range name { 32 | d.Write([]byte(part)) 33 | d.Write([]byte{'\x00'}) 34 | } 35 | hash := d.Sum(nil) 36 | suffix := hex.EncodeToString(hash[:])[:8] 37 | return SafeConcatName(append(name, suffix)...) 38 | } 39 | -------------------------------------------------------------------------------- /pkg/randomtoken/token.go: -------------------------------------------------------------------------------- 1 | package randomtoken 2 | 3 | import ( 4 | "crypto/rand" 5 | "math/big" 6 | ) 7 | 8 | const ( 9 | characters = "bcdfghjklmnpqrstvwxz2456789" 10 | tokenLength = 54 11 | ) 12 | 13 | var charsLength = big.NewInt(int64(len(characters))) 14 | 15 | func Generate() (string, error) { 16 | token := make([]byte, tokenLength) 17 | for i := range token { 18 | r, err := rand.Int(rand.Reader, charsLength) 19 | if err != nil { 20 | return "", err 21 | } 22 | token[i] = characters[r.Int64()] 23 | } 24 | return string(token), nil 25 | } 26 | -------------------------------------------------------------------------------- /pkg/ratelimit/none.go: -------------------------------------------------------------------------------- 1 | package ratelimit 2 | 3 | import ( 4 | "context" 5 | 6 | "k8s.io/client-go/util/flowcontrol" 7 | ) 8 | 9 | var ( 10 | None = flowcontrol.RateLimiter((*none)(nil)) 11 | ) 12 | 13 | type none struct{} 14 | 15 | func (*none) TryAccept() bool { return true } 16 | func (*none) Stop() {} 17 | func (*none) Accept() {} 18 | func (*none) QPS() float32 { return 1 } 19 | func (*none) Wait(_ context.Context) error { return nil } 20 | -------------------------------------------------------------------------------- /pkg/restconfig/restconfig.go: -------------------------------------------------------------------------------- 1 | package restconfig 2 | 3 | import ( 4 | "net/url" 5 | "os" 6 | 7 | "github.com/acorn-io/baaah/pkg/ratelimit" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "k8s.io/apimachinery/pkg/runtime/serializer" 10 | "k8s.io/client-go/kubernetes/scheme" 11 | "k8s.io/client-go/rest" 12 | "k8s.io/client-go/tools/clientcmd" 13 | "sigs.k8s.io/controller-runtime/pkg/client/config" 14 | ) 15 | 16 | func Default() (*rest.Config, error) { 17 | return New(scheme.Scheme) 18 | } 19 | 20 | func ClientConfigFromFile(file, context string) clientcmd.ClientConfig { 21 | loader := clientcmd.NewDefaultClientConfigLoadingRules() 22 | loader.ExplicitPath = file 23 | return clientcmd.NewNonInteractiveDeferredLoadingClientConfig( 24 | loader, 25 | &clientcmd.ConfigOverrides{ 26 | CurrentContext: context, 27 | }) 28 | } 29 | 30 | func FromFile(file, context string) (*rest.Config, error) { 31 | return ClientConfigFromFile(file, context).ClientConfig() 32 | } 33 | 34 | func SetScheme(cfg *rest.Config, scheme *runtime.Scheme) *rest.Config { 35 | cfg.NegotiatedSerializer = serializer.NewCodecFactory(scheme) 36 | cfg.UserAgent = rest.DefaultKubernetesUserAgent() 37 | return cfg 38 | } 39 | 40 | func New(scheme *runtime.Scheme) (*rest.Config, error) { 41 | cfg, err := config.GetConfigWithContext(os.Getenv("CONTEXT")) 42 | if err != nil { 43 | return nil, err 44 | } 45 | cfg.RateLimiter = ratelimit.None 46 | return SetScheme(cfg, scheme), nil 47 | } 48 | 49 | func FromURLTokenAndScheme(serverURL, token string, scheme *runtime.Scheme) (*rest.Config, error) { 50 | u, err := url.Parse(serverURL) 51 | if err != nil { 52 | return nil, err 53 | } 54 | insecure := false 55 | if u.Scheme == "https" && u.Host == "localhost" { 56 | insecure = true 57 | } 58 | 59 | cfg := &rest.Config{ 60 | Host: serverURL, 61 | TLSClientConfig: rest.TLSClientConfig{ 62 | Insecure: insecure, 63 | }, 64 | BearerToken: token, 65 | RateLimiter: ratelimit.None, 66 | } 67 | 68 | SetScheme(cfg, scheme) 69 | return cfg, nil 70 | } 71 | -------------------------------------------------------------------------------- /pkg/restconfig/scheme.go: -------------------------------------------------------------------------------- 1 | package restconfig 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "k8s.io/apimachinery/pkg/runtime/schema" 10 | "k8s.io/apimachinery/pkg/runtime/serializer" 11 | ) 12 | 13 | type AddToSchemeFunc func(scheme *runtime.Scheme) error 14 | 15 | func MustBuildScheme(addToSchemes ...AddToSchemeFunc) (*runtime.Scheme, serializer.CodecFactory, runtime.ParameterCodec, func(*runtime.Scheme) error) { 16 | scheme := runtime.NewScheme() 17 | addToScheme := AddToSchemeFunc(func(scheme *runtime.Scheme) error { 18 | var errs []error 19 | metav1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) 20 | for _, f := range addToSchemes { 21 | err := f(scheme) 22 | if err != nil { 23 | errs = append(errs, err) 24 | } 25 | } 26 | return errors.Join(errs...) 27 | }) 28 | 29 | if err := addToScheme(scheme); err != nil { 30 | panic(fmt.Errorf("failed to build scheme: %w", err)) 31 | } 32 | 33 | return scheme, 34 | serializer.NewCodecFactory(scheme), 35 | runtime.NewParameterCodec(scheme), 36 | addToScheme 37 | } 38 | -------------------------------------------------------------------------------- /pkg/restconfig/wait.go: -------------------------------------------------------------------------------- 1 | package restconfig 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/sirupsen/logrus" 10 | "k8s.io/client-go/rest" 11 | ) 12 | 13 | func WaitFor(ctx context.Context, cfg *rest.Config) error { 14 | ctx, cancel := context.WithTimeout(ctx, 2*time.Minute) 15 | defer cancel() 16 | 17 | start := time.Now() 18 | log := logrus.WithField("server", cfg.Host) 19 | log.Info("Waiting for Kubernetes API to be ready") 20 | 21 | cli, err := rest.UnversionedRESTClientFor(cfg) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | for { 27 | select { 28 | case <-ctx.Done(): 29 | // Return close error along with the last ready error, if any 30 | return errors.Join(fmt.Errorf("context closed before %q was ready: %w", cfg.Host, ctx.Err()), err) 31 | default: 32 | } 33 | 34 | resp := cli.Get().AbsPath("/readyz").Do(ctx) 35 | log = log.WithField("elapsed", time.Since(start)) 36 | if err = resp.Error(); err == nil { 37 | log.Info("Kubernetes API ready") 38 | break 39 | } 40 | 41 | log.WithError(err).Debug("Kubernetes API not ready") 42 | time.Sleep(2 * time.Second) 43 | } 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /pkg/router/client.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/acorn-io/baaah/pkg/backend" 7 | "k8s.io/apimachinery/pkg/api/meta" 8 | "k8s.io/apimachinery/pkg/fields" 9 | "k8s.io/apimachinery/pkg/labels" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | "k8s.io/apimachinery/pkg/runtime/schema" 12 | "k8s.io/apimachinery/pkg/watch" 13 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 14 | ) 15 | 16 | type TriggerRegistry interface { 17 | Watch(obj runtime.Object, namespace, name string, selector labels.Selector, fields fields.Selector) error 18 | WatchingGVKs() []schema.GroupVersionKind 19 | } 20 | 21 | type client struct { 22 | backend backend.Backend 23 | reader 24 | writer 25 | status 26 | } 27 | 28 | func (c *client) Watch(ctx context.Context, list kclient.ObjectList, opts ...kclient.ListOption) (watch.Interface, error) { 29 | return c.backend.Watch(ctx, list, opts...) 30 | } 31 | 32 | func (c *client) Scheme() *runtime.Scheme { 33 | return c.reader.client.Scheme() 34 | } 35 | 36 | func (c *client) RESTMapper() meta.RESTMapper { 37 | return c.reader.client.RESTMapper() 38 | } 39 | 40 | type writer struct { 41 | client kclient.Client 42 | registry TriggerRegistry 43 | } 44 | 45 | func (w *writer) DeleteAllOf(ctx context.Context, obj kclient.Object, opts ...kclient.DeleteAllOfOption) error { 46 | delOpts := &kclient.DeleteAllOfOptions{} 47 | for _, opt := range opts { 48 | opt.ApplyToDeleteAllOf(delOpts) 49 | } 50 | if err := w.registry.Watch(obj, delOpts.Namespace, "", delOpts.LabelSelector, delOpts.FieldSelector); err != nil { 51 | return err 52 | } 53 | return w.client.DeleteAllOf(ctx, obj, opts...) 54 | } 55 | 56 | func (w *writer) Delete(ctx context.Context, obj kclient.Object, opts ...kclient.DeleteOption) error { 57 | if err := w.registry.Watch(obj, obj.GetNamespace(), obj.GetName(), nil, nil); err != nil { 58 | return err 59 | } 60 | return w.client.Delete(ctx, obj, opts...) 61 | } 62 | 63 | func (w *writer) Patch(ctx context.Context, obj kclient.Object, patch kclient.Patch, opts ...kclient.PatchOption) error { 64 | if err := w.registry.Watch(obj, obj.GetNamespace(), obj.GetName(), nil, nil); err != nil { 65 | return err 66 | } 67 | return w.client.Patch(ctx, obj, patch, opts...) 68 | } 69 | 70 | func (w *writer) Update(ctx context.Context, obj kclient.Object, opts ...kclient.UpdateOption) error { 71 | if err := w.registry.Watch(obj, obj.GetNamespace(), obj.GetName(), nil, nil); err != nil { 72 | return err 73 | } 74 | return w.client.Update(ctx, obj, opts...) 75 | } 76 | 77 | func (w *writer) Create(ctx context.Context, obj kclient.Object, opts ...kclient.CreateOption) error { 78 | if err := w.registry.Watch(obj, obj.GetNamespace(), obj.GetName(), nil, nil); err != nil { 79 | return err 80 | } 81 | return w.client.Create(ctx, obj, opts...) 82 | } 83 | 84 | type subResourceClient struct { 85 | writer kclient.SubResourceWriter 86 | reader kclient.SubResourceReader 87 | registry TriggerRegistry 88 | } 89 | 90 | type status struct { 91 | client kclient.Client 92 | registry TriggerRegistry 93 | } 94 | 95 | func (s *status) Status() kclient.StatusWriter { 96 | return &subResourceClient{ 97 | writer: s.client.Status(), 98 | registry: s.registry, 99 | } 100 | } 101 | 102 | func (s *subResourceClient) Get(ctx context.Context, obj kclient.Object, subResource kclient.Object, opts ...kclient.SubResourceGetOption) error { 103 | if err := s.registry.Watch(obj, obj.GetNamespace(), obj.GetName(), nil, nil); err != nil { 104 | return err 105 | } 106 | 107 | return s.reader.Get(ctx, obj, subResource, opts...) 108 | } 109 | 110 | func (s *subResourceClient) Update(ctx context.Context, obj kclient.Object, opts ...kclient.SubResourceUpdateOption) error { 111 | if err := s.registry.Watch(obj, obj.GetNamespace(), obj.GetName(), nil, nil); err != nil { 112 | return err 113 | } 114 | return s.writer.Update(ctx, obj, opts...) 115 | } 116 | 117 | func (s *subResourceClient) Patch(ctx context.Context, obj kclient.Object, patch kclient.Patch, opts ...kclient.SubResourcePatchOption) error { 118 | if err := s.registry.Watch(obj, obj.GetNamespace(), obj.GetName(), nil, nil); err != nil { 119 | return err 120 | } 121 | return s.writer.Patch(ctx, obj, patch, opts...) 122 | } 123 | 124 | func (s *subResourceClient) Create(ctx context.Context, obj kclient.Object, subResource kclient.Object, opts ...kclient.SubResourceCreateOption) error { 125 | if err := s.registry.Watch(obj, obj.GetNamespace(), obj.GetName(), nil, nil); err != nil { 126 | return err 127 | } 128 | return s.writer.Create(ctx, obj, subResource, opts...) 129 | } 130 | 131 | type reader struct { 132 | scheme *runtime.Scheme 133 | client kclient.Client 134 | registry TriggerRegistry 135 | } 136 | 137 | func (a *reader) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) { 138 | return a.client.GroupVersionKindFor(obj) 139 | } 140 | 141 | func (a *reader) IsObjectNamespaced(obj runtime.Object) (bool, error) { 142 | return a.client.IsObjectNamespaced(obj) 143 | } 144 | 145 | func (a *reader) SubResource(subResource string) kclient.SubResourceClient { 146 | c := a.client.SubResource(subResource) 147 | return &subResourceClient{ 148 | writer: c, 149 | reader: c, 150 | registry: a.registry, 151 | } 152 | } 153 | 154 | func (a *reader) Get(ctx context.Context, key kclient.ObjectKey, obj kclient.Object, opts ...kclient.GetOption) error { 155 | if err := a.registry.Watch(obj, key.Namespace, key.Name, nil, nil); err != nil { 156 | return err 157 | } 158 | 159 | return a.client.Get(ctx, key, obj, opts...) 160 | } 161 | 162 | func (a *reader) List(ctx context.Context, list kclient.ObjectList, opts ...kclient.ListOption) error { 163 | listOpt := &kclient.ListOptions{} 164 | for _, opt := range opts { 165 | opt.ApplyToList(listOpt) 166 | } 167 | 168 | if err := a.registry.Watch(list, listOpt.Namespace, "", listOpt.LabelSelector, listOpt.FieldSelector); err != nil { 169 | return err 170 | } 171 | 172 | return a.client.List(ctx, list, listOpt) 173 | } 174 | -------------------------------------------------------------------------------- /pkg/router/finalizer.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "k8s.io/utils/strings/slices" 5 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 6 | ) 7 | 8 | type FinalizerHandler struct { 9 | FinalizerID string 10 | Next Handler 11 | } 12 | 13 | func (f FinalizerHandler) Handle(req Request, resp Response) error { 14 | obj := req.Object 15 | if obj == nil { 16 | return nil 17 | } 18 | 19 | if obj.GetDeletionTimestamp().IsZero() { 20 | if !slices.Contains(obj.GetFinalizers(), f.FinalizerID) { 21 | obj.SetFinalizers(append(obj.GetFinalizers(), f.FinalizerID)) 22 | if err := req.Client.Update(req.Ctx, obj); err != nil { 23 | return err 24 | } 25 | resp.Objects(obj) 26 | } 27 | return nil 28 | } 29 | 30 | if len(obj.GetFinalizers()) == 0 || obj.GetFinalizers()[0] != f.FinalizerID { 31 | return nil 32 | } 33 | 34 | newResp := &ResponseWrapper{} 35 | newObj := obj.DeepCopyObject().(kclient.Object) 36 | req.Object = newObj 37 | 38 | if err := f.Next.Handle(req, newResp); err != nil { 39 | return err 40 | } 41 | 42 | if newResp.Delay != 0 { 43 | resp.RetryAfter(newResp.Delay) 44 | } 45 | if newResp.NoPrune { 46 | resp.DisablePrune() 47 | } 48 | for k, v := range newResp.Attr { 49 | resp.Attributes()[k] = v 50 | } 51 | 52 | for _, respObj := range newResp.Objs { 53 | if ok, err := isObjectForRequest(req, respObj); err != nil { 54 | return err 55 | } else if ok { 56 | newObj = respObj 57 | } 58 | resp.Objects(respObj) 59 | } 60 | 61 | if StatusChanged(obj, newObj) { 62 | if err := req.Client.Status().Update(req.Ctx, newObj); err != nil { 63 | return err 64 | } 65 | } 66 | 67 | if newResp.Delay == 0 && len(newObj.GetFinalizers()) > 0 && newObj.GetFinalizers()[0] == f.FinalizerID { 68 | newObj.SetFinalizers(obj.GetFinalizers()[1:]) 69 | if err := req.Client.Update(req.Ctx, newObj); err != nil { 70 | return err 71 | } 72 | resp.Objects(newObj) 73 | } 74 | 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /pkg/router/getorcreate.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "context" 5 | 6 | apierror "k8s.io/apimachinery/pkg/api/errors" 7 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 8 | ) 9 | 10 | func GetOrCreate(ctx context.Context, client kclient.Client, obj kclient.Object, prepare func() error) error { 11 | ns, name := obj.GetNamespace(), obj.GetName() 12 | err := client.Get(ctx, kclient.ObjectKey{ 13 | Namespace: ns, 14 | Name: name, 15 | }, obj) 16 | if apierror.IsNotFound(err) { 17 | if prepare != nil { 18 | err := prepare() 19 | if err != nil { 20 | return err 21 | } 22 | } 23 | err = client.Create(ctx, obj) 24 | if apierror.IsAlreadyExists(err) { 25 | return client.Get(ctx, kclient.ObjectKey{ 26 | Namespace: ns, 27 | Name: name, 28 | }, obj) 29 | } 30 | } 31 | return err 32 | } 33 | -------------------------------------------------------------------------------- /pkg/router/handler.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "sync" 8 | "time" 9 | 10 | "github.com/acorn-io/baaah/pkg/apply" 11 | "github.com/acorn-io/baaah/pkg/backend" 12 | "github.com/acorn-io/baaah/pkg/log" 13 | "github.com/acorn-io/baaah/pkg/merr" 14 | "github.com/moby/locker" 15 | "golang.org/x/exp/maps" 16 | "golang.org/x/time/rate" 17 | apierror "k8s.io/apimachinery/pkg/api/errors" 18 | "k8s.io/apimachinery/pkg/fields" 19 | "k8s.io/apimachinery/pkg/labels" 20 | "k8s.io/apimachinery/pkg/runtime" 21 | "k8s.io/apimachinery/pkg/runtime/schema" 22 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 23 | ) 24 | 25 | const ( 26 | TriggerPrefix = "_t " 27 | ReplayPrefix = "_r " 28 | ) 29 | 30 | type HandlerSet struct { 31 | ctx context.Context 32 | name string 33 | scheme *runtime.Scheme 34 | backend backend.Backend 35 | handlers handlers 36 | triggers triggers 37 | save save 38 | onError ErrorHandler 39 | 40 | watchingLock sync.Mutex 41 | watching map[schema.GroupVersionKind]bool 42 | locker locker.Locker 43 | 44 | limiterLock sync.Mutex 45 | limiters map[limiterKey]*rate.Limiter 46 | waiting map[limiterKey]struct{} 47 | } 48 | 49 | type limiterKey struct { 50 | key string 51 | gvk schema.GroupVersionKind 52 | } 53 | 54 | func NewHandlerSet(name string, scheme *runtime.Scheme, backend backend.Backend) *HandlerSet { 55 | hs := &HandlerSet{ 56 | name: name, 57 | scheme: scheme, 58 | backend: backend, 59 | handlers: handlers{ 60 | handlers: map[schema.GroupVersionKind][]Handler{}, 61 | }, 62 | triggers: triggers{ 63 | matchers: map[schema.GroupVersionKind]map[enqueueTarget][]objectMatcher{}, 64 | trigger: backend, 65 | gvkLookup: backend, 66 | scheme: scheme, 67 | }, 68 | save: save{ 69 | apply: apply.New(backend).WithOwnerSubContext(name), 70 | cache: backend, 71 | client: backend, 72 | }, 73 | watching: map[schema.GroupVersionKind]bool{}, 74 | } 75 | hs.triggers.watcher = hs 76 | return hs 77 | } 78 | 79 | func (m *HandlerSet) Start(ctx context.Context) error { 80 | m.ctx = ctx 81 | if err := m.WatchGVK(m.handlers.GVKs()...); err != nil { 82 | return err 83 | } 84 | return m.backend.Start(ctx) 85 | } 86 | 87 | func toObject(obj runtime.Object) kclient.Object { 88 | if obj == nil { 89 | return nil 90 | } 91 | // yep panic if it's not this interface 92 | return obj.DeepCopyObject().(kclient.Object) 93 | } 94 | 95 | type triggerRegistry struct { 96 | gvk schema.GroupVersionKind 97 | gvks map[schema.GroupVersionKind]bool 98 | key string 99 | trigger *triggers 100 | } 101 | 102 | func (t *triggerRegistry) WatchingGVKs() []schema.GroupVersionKind { 103 | return maps.Keys(t.gvks) 104 | 105 | } 106 | func (t *triggerRegistry) Watch(obj runtime.Object, namespace, name string, sel labels.Selector, fields fields.Selector) error { 107 | gvk, ok, err := t.trigger.Register(t.gvk, t.key, obj, namespace, name, sel, fields) 108 | if err != nil { 109 | return err 110 | } 111 | if ok { 112 | t.gvks[gvk] = true 113 | } 114 | return nil 115 | } 116 | 117 | func (m *HandlerSet) newRequestResponse(gvk schema.GroupVersionKind, key string, runtimeObject runtime.Object, trigger bool) (Request, *response, error) { 118 | var ( 119 | obj = toObject(runtimeObject) 120 | ) 121 | 122 | ns, name, ok := strings.Cut(key, "/") 123 | if !ok { 124 | name = key 125 | ns = "" 126 | } 127 | 128 | triggerRegistry := &triggerRegistry{ 129 | gvk: gvk, 130 | key: key, 131 | trigger: &m.triggers, 132 | gvks: map[schema.GroupVersionKind]bool{}, 133 | } 134 | 135 | resp := response{ 136 | registry: triggerRegistry, 137 | } 138 | 139 | req := Request{ 140 | FromTrigger: trigger, 141 | Client: &client{ 142 | backend: m.backend, 143 | reader: reader{ 144 | scheme: m.scheme, 145 | client: m.backend, 146 | registry: triggerRegistry, 147 | }, 148 | writer: writer{ 149 | client: m.backend, 150 | registry: triggerRegistry, 151 | }, 152 | status: status{ 153 | client: m.backend, 154 | registry: triggerRegistry, 155 | }, 156 | }, 157 | Ctx: m.ctx, 158 | GVK: gvk, 159 | Object: obj, 160 | Namespace: ns, 161 | Name: name, 162 | Key: key, 163 | } 164 | 165 | return req, &resp, nil 166 | } 167 | 168 | func (m *HandlerSet) AddHandler(objType kclient.Object, handler Handler) { 169 | gvk, err := m.backend.GVKForObject(objType, m.scheme) 170 | if err != nil { 171 | panic(fmt.Sprintf("scheme does not know gvk for %T", objType)) 172 | } 173 | m.handlers.AddHandler(gvk, handler) 174 | } 175 | 176 | func (m *HandlerSet) WatchGVK(gvks ...schema.GroupVersionKind) error { 177 | var watchErrs []error 178 | m.watchingLock.Lock() 179 | for _, gvk := range gvks { 180 | if m.watching[gvk] { 181 | continue 182 | } 183 | if err := m.backend.Watcher(m.ctx, gvk, m.name, m.onChange); err == nil { 184 | m.watching[gvk] = true 185 | } else { 186 | watchErrs = append(watchErrs, err) 187 | } 188 | } 189 | m.watchingLock.Unlock() 190 | return merr.NewErrors(watchErrs...) 191 | } 192 | 193 | func (m *HandlerSet) checkDelay(gvk schema.GroupVersionKind, key string) bool { 194 | m.limiterLock.Lock() 195 | defer m.limiterLock.Unlock() 196 | lKey := limiterKey{key: key, gvk: gvk} 197 | 198 | if _, ok := m.waiting[lKey]; ok { 199 | return false 200 | } 201 | 202 | limit, ok := m.limiters[lKey] 203 | if !ok { 204 | // Limit to once every 15 seconds with a burst of 10. This limits the 205 | // overall rate at which we can process a key regardless of the key 206 | // source (change event, trigger, error re-enqueue) 207 | limit = rate.NewLimiter(rate.Limit(1.0/5), 10) 208 | if m.limiters == nil { 209 | m.limiters = map[limiterKey]*rate.Limiter{} 210 | } 211 | m.limiters[lKey] = limit 212 | } 213 | 214 | delay := limit.Reserve().Delay() 215 | if delay > 0 { 216 | if m.waiting == nil { 217 | m.waiting = map[limiterKey]struct{}{} 218 | } 219 | m.waiting[lKey] = struct{}{} 220 | go func() { 221 | log.Debugf("Backing off [%s] [%s] for %s", key, gvk, delay) 222 | time.Sleep(delay) 223 | m.limiterLock.Lock() 224 | defer m.limiterLock.Unlock() 225 | delete(m.waiting, lKey) 226 | _ = m.backend.Trigger(gvk, ReplayPrefix+key, 0) 227 | }() 228 | return false 229 | } 230 | 231 | return true 232 | } 233 | 234 | func (m *HandlerSet) forgetBackoff(gvk schema.GroupVersionKind, key string) { 235 | m.limiterLock.Lock() 236 | defer m.limiterLock.Unlock() 237 | delete(m.limiters, limiterKey{key: key, gvk: gvk}) 238 | } 239 | 240 | func (m *HandlerSet) onChange(gvk schema.GroupVersionKind, key string, runtimeObject runtime.Object) (runtime.Object, error) { 241 | fromTrigger := false 242 | fromReplay := false 243 | if strings.HasPrefix(key, TriggerPrefix) { 244 | fromTrigger = true 245 | key = strings.TrimPrefix(key, TriggerPrefix) 246 | } 247 | if strings.HasPrefix(key, ReplayPrefix) { 248 | fromTrigger = false 249 | fromReplay = true 250 | key = strings.TrimPrefix(key, ReplayPrefix) 251 | } 252 | 253 | if !fromReplay && !fromTrigger { 254 | // Process delay have key has be reassigned from the TriggerPrefix 255 | if !m.checkDelay(gvk, key) { 256 | return runtimeObject, nil 257 | } 258 | } 259 | 260 | obj, err := m.scheme.New(gvk) 261 | if err != nil { 262 | return nil, err 263 | } 264 | 265 | ns, name, ok := strings.Cut(key, "/") 266 | if !ok { 267 | name = key 268 | ns = "" 269 | } 270 | 271 | lockKey := gvk.Kind + " " + key 272 | m.locker.Lock(lockKey) 273 | defer func() { _ = m.locker.Unlock(lockKey) }() 274 | 275 | err = m.backend.Get(m.ctx, kclient.ObjectKey{Name: name, Namespace: ns}, obj.(kclient.Object)) 276 | if err == nil { 277 | runtimeObject = obj 278 | } else if !apierror.IsNotFound(err) { 279 | return nil, err 280 | } 281 | 282 | if runtimeObject == nil { 283 | m.forgetBackoff(gvk, key) 284 | } 285 | 286 | return m.handle(gvk, key, runtimeObject, fromTrigger) 287 | } 288 | 289 | func (m *HandlerSet) handleError(req Request, resp Response, err error) error { 290 | if m.onError != nil { 291 | return m.onError(req, resp, err) 292 | } 293 | return err 294 | } 295 | 296 | func (m *HandlerSet) handle(gvk schema.GroupVersionKind, key string, unmodifiedObject runtime.Object, trigger bool) (runtime.Object, error) { 297 | req, resp, err := m.newRequestResponse(gvk, key, unmodifiedObject, trigger) 298 | if err != nil { 299 | return nil, err 300 | } 301 | 302 | handles := m.handlers.Handles(req) 303 | if handles { 304 | if req.FromTrigger { 305 | log.Debugf("Handling trigger [%s/%s] [%v]", req.Namespace, req.Name, req.GVK) 306 | } else { 307 | log.Debugf("Handling [%s/%s] [%v]", req.Namespace, req.Name, req.GVK) 308 | } 309 | 310 | if err := m.handlers.Handle(req, resp); err != nil { 311 | if err := m.handleError(req, resp, err); err != nil { 312 | return nil, err 313 | } 314 | } 315 | } 316 | 317 | if unmodifiedObject == nil { 318 | // A nil object here means tha the object was deleted, so unregister the triggers 319 | m.triggers.UnregisterAndTrigger(req) 320 | } else { 321 | m.triggers.Trigger(req) 322 | } 323 | 324 | if handles { 325 | m.watchingLock.Lock() 326 | keys := maps.Keys(m.watching) 327 | m.watchingLock.Unlock() 328 | newObj, err := m.save.save(unmodifiedObject, req, resp, keys) 329 | if err != nil { 330 | if err := m.handleError(req, resp, err); err != nil { 331 | return nil, err 332 | } 333 | } 334 | req.Object = newObj 335 | 336 | if resp.delay > 0 { 337 | if err := m.backend.Trigger(gvk, key, resp.delay); err != nil { 338 | return nil, err 339 | } 340 | } 341 | } 342 | 343 | return req.Object, m.handleError(req, resp, err) 344 | } 345 | 346 | type ResponseAttributes struct { 347 | attr map[string]any 348 | } 349 | 350 | func (r *ResponseAttributes) Attributes() map[string]any { 351 | if r.attr == nil { 352 | r.attr = map[string]any{} 353 | } 354 | return r.attr 355 | } 356 | 357 | type response struct { 358 | ResponseAttributes 359 | 360 | delay time.Duration 361 | objects []kclient.Object 362 | registry TriggerRegistry 363 | noPrune bool 364 | } 365 | 366 | func (r *response) DisablePrune() { 367 | r.noPrune = true 368 | } 369 | 370 | func (r *response) RetryAfter(delay time.Duration) { 371 | if r.delay == 0 || delay < r.delay { 372 | r.delay = delay 373 | } 374 | } 375 | 376 | func (r *response) Objects(objs ...kclient.Object) { 377 | for _, obj := range objs { 378 | _ = r.registry.Watch(obj, obj.GetNamespace(), obj.GetName(), nil, nil) 379 | r.objects = append(r.objects, obj) 380 | } 381 | } 382 | 383 | func (r *response) WatchingGVKs() []schema.GroupVersionKind { 384 | return r.registry.WatchingGVKs() 385 | } 386 | -------------------------------------------------------------------------------- /pkg/router/handlers.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/acorn-io/baaah/pkg/merr" 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 9 | ) 10 | 11 | func isObjectForRequest(req Request, obj kclient.Object) (bool, error) { 12 | gvk, err := req.Client.GroupVersionKindFor(obj) 13 | if err != nil { 14 | return false, err 15 | } 16 | return obj.GetName() == req.Name && 17 | obj.GetNamespace() == req.Namespace && 18 | gvk == req.GVK, nil 19 | } 20 | 21 | type handlers struct { 22 | lock sync.RWMutex 23 | handlers map[schema.GroupVersionKind][]Handler 24 | } 25 | 26 | func (h *handlers) GVKs() (result []schema.GroupVersionKind) { 27 | for gvk := range h.handlers { 28 | result = append(result, gvk) 29 | } 30 | return result 31 | } 32 | 33 | func (h *handlers) AddHandler(gvk schema.GroupVersionKind, handler Handler) { 34 | h.lock.Lock() 35 | defer h.lock.Unlock() 36 | h.handlers[gvk] = append(h.handlers[gvk], handler) 37 | } 38 | 39 | func (h *handlers) Handles(req Request) bool { 40 | h.lock.RLock() 41 | defer h.lock.RUnlock() 42 | return len(h.handlers[req.GVK]) > 0 43 | } 44 | 45 | func (h *handlers) Handle(req Request, resp *response) error { 46 | h.lock.RLock() 47 | var ( 48 | errs []error 49 | handlers = h.handlers[req.GVK] 50 | ) 51 | h.lock.RUnlock() 52 | 53 | for _, h := range handlers { 54 | err := h.Handle(req, resp) 55 | if err != nil { 56 | errs = append(errs, err) 57 | } 58 | newObjects := make([]kclient.Object, 0, len(resp.objects)) 59 | for _, obj := range resp.objects { 60 | if ok, err := isObjectForRequest(req, obj); err != nil { 61 | return err 62 | } else if ok { 63 | req.Object = obj 64 | } else { 65 | newObjects = append(newObjects, obj) 66 | } 67 | } 68 | resp.objects = newObjects 69 | } 70 | return merr.NewErrors(errs...) 71 | } 72 | -------------------------------------------------------------------------------- /pkg/router/healthz.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "os/signal" 8 | "sync" 9 | "syscall" 10 | 11 | "github.com/acorn-io/baaah/pkg/log" 12 | ) 13 | 14 | var healthz struct { 15 | healths map[string]bool 16 | started bool 17 | lock *sync.RWMutex 18 | port int 19 | } 20 | 21 | func init() { 22 | healthz.lock = &sync.RWMutex{} 23 | healthz.healths = make(map[string]bool) 24 | } 25 | 26 | func setPort(port int) { 27 | healthz.lock.Lock() 28 | defer healthz.lock.Unlock() 29 | if healthz.port > 0 { 30 | log.Warnf("healthz port cannot be changed") 31 | return 32 | } 33 | healthz.port = port 34 | } 35 | 36 | func setHealthy(name string, healthy bool) { 37 | healthz.lock.Lock() 38 | defer healthz.lock.Unlock() 39 | healthz.healths[name] = healthy 40 | } 41 | 42 | func getHealthy() bool { 43 | healthz.lock.RLock() 44 | defer healthz.lock.RUnlock() 45 | for _, healthy := range healthz.healths { 46 | if !healthy { 47 | return false 48 | } 49 | } 50 | return true 51 | } 52 | 53 | // startHealthz starts a healthz server on the healthzPort. If the server is already running, then this is a no-op. 54 | // Similarly, if the healthzPort is <= 0, then this is a no-op. 55 | func startHealthz(ctx context.Context) { 56 | healthz.lock.Lock() 57 | defer healthz.lock.Unlock() 58 | if healthz.started || healthz.port <= 0 { 59 | return 60 | } 61 | healthz.started = true 62 | 63 | // Catch these signals to ensure a graceful shutdown of the server. 64 | sigCtx, cancel := signal.NotifyContext(ctx, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGKILL) 65 | 66 | mux := http.NewServeMux() 67 | mux.HandleFunc("/healthz", func(w http.ResponseWriter, req *http.Request) { 68 | if getHealthy() { 69 | w.WriteHeader(http.StatusOK) 70 | return 71 | } 72 | w.WriteHeader(http.StatusServiceUnavailable) 73 | }) 74 | 75 | srv := &http.Server{ 76 | Addr: fmt.Sprintf(":%d", healthz.port), 77 | Handler: mux, 78 | } 79 | go func() { 80 | <-sigCtx.Done() 81 | // Must cancel so that the registered signals are no longer caught. 82 | cancel() 83 | if err := srv.Shutdown(ctx); err != nil { 84 | log.Warnf("error shutting down healthz server: %v", err) 85 | } 86 | }() 87 | go func() { 88 | log.Infof("healthz server stopped: %v", srv.ListenAndServe()) 89 | }() 90 | } 91 | -------------------------------------------------------------------------------- /pkg/router/matcher.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/fields" 5 | "k8s.io/apimachinery/pkg/labels" 6 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 7 | ) 8 | 9 | type objectMatcher struct { 10 | Namespace string 11 | Name string 12 | Selector labels.Selector 13 | Fields fields.Selector 14 | } 15 | 16 | func (o *objectMatcher) Equals(other objectMatcher) bool { 17 | if o.Name != other.Name { 18 | return false 19 | } 20 | if o.Namespace != other.Namespace { 21 | return false 22 | } 23 | if (o.Selector == nil) != (other.Selector == nil) { 24 | return false 25 | } 26 | if o.Selector != nil && o.Selector.String() != other.Selector.String() { 27 | return false 28 | } 29 | if (o.Fields == nil) != (other.Fields == nil) { 30 | return false 31 | } 32 | if o.Fields != nil && o.Fields.String() != other.Fields.String() { 33 | return false 34 | } 35 | return true 36 | } 37 | 38 | func (o *objectMatcher) Match(ns, name string, obj kclient.Object) bool { 39 | if o.Name != "" { 40 | return o.Name == name && 41 | o.Namespace == ns 42 | } 43 | if o.Namespace != "" && o.Namespace != ns { 44 | return false 45 | } 46 | if o.Selector != nil || o.Fields != nil { 47 | if obj == nil { 48 | return false 49 | } 50 | var ( 51 | selectorMatches = true 52 | fieldMatches = true 53 | ) 54 | if o.Selector != nil { 55 | selectorMatches = o.Selector.Matches(labels.Set(obj.GetLabels())) 56 | } 57 | if o.Fields != nil { 58 | if i, ok := obj.(fields.Fields); ok { 59 | fieldMatches = o.Fields.Matches(i) 60 | } 61 | } 62 | return selectorMatches && fieldMatches 63 | } 64 | if o.Fields != nil { 65 | if obj == nil { 66 | return false 67 | } 68 | if i, ok := obj.(fields.Fields); ok { 69 | return o.Fields.Matches(i) 70 | } 71 | } 72 | return o.Namespace == "" || o.Namespace == ns 73 | } 74 | -------------------------------------------------------------------------------- /pkg/router/response_wrapper.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "time" 5 | 6 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 7 | ) 8 | 9 | type ResponseWrapper struct { 10 | NoPrune bool 11 | Delay time.Duration 12 | Objs []kclient.Object 13 | Attr map[string]any 14 | } 15 | 16 | func (r *ResponseWrapper) Attributes() map[string]any { 17 | if r.Attr == nil { 18 | r.Attr = map[string]any{} 19 | } 20 | return r.Attr 21 | } 22 | 23 | func (r *ResponseWrapper) DisablePrune() { 24 | r.NoPrune = true 25 | } 26 | 27 | func (r *ResponseWrapper) RetryAfter(delay time.Duration) { 28 | r.Delay = delay 29 | } 30 | 31 | func (r *ResponseWrapper) Objects(obj ...kclient.Object) { 32 | r.Objs = append(r.Objs, obj...) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "runtime" 9 | 10 | "github.com/acorn-io/baaah/pkg/backend" 11 | "github.com/acorn-io/baaah/pkg/leader" 12 | "k8s.io/apimachinery/pkg/fields" 13 | "k8s.io/apimachinery/pkg/labels" 14 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 15 | ) 16 | 17 | type Router struct { 18 | RouteBuilder 19 | 20 | OnErrorHandler ErrorHandler 21 | handlers *HandlerSet 22 | electionConfig *leader.ElectionConfig 23 | hasHealthz bool 24 | } 25 | 26 | // New returns a new *Router with given HandlerSet and ElectionConfig. Passing a nil ElectionConfig is valid and results 27 | // in no leader election for the router. 28 | // The healthzPort is the port on which the healthz endpoint will be served. If <= 0, the healthz endpoint will not be 29 | // served. When creating multiple routers, the first router created with a positive healthzPort will be used. 30 | // The healthz endpoint is served on /healthz, and will not be started until the router is started. 31 | func New(handlerSet *HandlerSet, electionConfig *leader.ElectionConfig, healthzPort int) *Router { 32 | r := &Router{ 33 | handlers: handlerSet, 34 | electionConfig: electionConfig, 35 | } 36 | 37 | if healthzPort > 0 { 38 | setPort(healthzPort) 39 | r.hasHealthz = true 40 | } 41 | 42 | r.RouteBuilder.router = r 43 | return r 44 | } 45 | 46 | func (r *Router) Backend() backend.Backend { 47 | return r.handlers.backend 48 | } 49 | 50 | type RouteBuilder struct { 51 | includeRemove bool 52 | includeFinalizing bool 53 | finalizeID string 54 | router *Router 55 | objType kclient.Object 56 | name string 57 | namespace string 58 | routeName string 59 | middleware []Middleware 60 | sel labels.Selector 61 | fieldSelector fields.Selector 62 | } 63 | 64 | func (r RouteBuilder) Middleware(m ...Middleware) RouteBuilder { 65 | r.middleware = append(r.middleware, m...) 66 | return r 67 | } 68 | 69 | func (r RouteBuilder) Namespace(namespace string) RouteBuilder { 70 | r.namespace = namespace 71 | return r 72 | } 73 | 74 | func (r RouteBuilder) Selector(sel labels.Selector) RouteBuilder { 75 | r.sel = sel 76 | return r 77 | } 78 | 79 | func (r RouteBuilder) FieldSelector(sel fields.Selector) RouteBuilder { 80 | r.fieldSelector = sel 81 | return r 82 | } 83 | 84 | func (r RouteBuilder) Name(name string) RouteBuilder { 85 | r.name = name 86 | return r 87 | } 88 | 89 | func (r RouteBuilder) IncludeRemoved() RouteBuilder { 90 | r.includeRemove = true 91 | return r 92 | } 93 | 94 | func (r RouteBuilder) IncludeFinalizing() RouteBuilder { 95 | r.includeFinalizing = true 96 | return r 97 | } 98 | 99 | func (r RouteBuilder) Finalize(finalizerID string, h Handler) { 100 | r.finalizeID = finalizerID 101 | r.routeName = name() 102 | r.Handler(h) 103 | } 104 | 105 | func name() string { 106 | _, filename, line, _ := runtime.Caller(2) 107 | return fmt.Sprintf("%s:%d", filepath.Base(filename), line) 108 | } 109 | 110 | func (r RouteBuilder) FinalizeFunc(finalizerID string, h HandlerFunc) { 111 | r.finalizeID = finalizerID 112 | r.routeName = name() 113 | r.Handler(h) 114 | } 115 | 116 | func (r RouteBuilder) Type(objType kclient.Object) RouteBuilder { 117 | r.objType = objType 118 | return r 119 | } 120 | 121 | func (r RouteBuilder) HandlerFunc(h HandlerFunc) { 122 | r.routeName = name() 123 | r.Handler(h) 124 | } 125 | 126 | func (r RouteBuilder) Handler(h Handler) { 127 | if r.routeName == "" { 128 | r.routeName = name() 129 | } 130 | result := h 131 | if r.finalizeID != "" { 132 | result = FinalizerHandler{ 133 | FinalizerID: r.finalizeID, 134 | Next: result, 135 | } 136 | } 137 | for i := len(r.middleware) - 1; i >= 0; i-- { 138 | result = r.middleware[i](result) 139 | } 140 | if r.name != "" || r.namespace != "" { 141 | result = NameNamespaceFilter{ 142 | Next: result, 143 | Name: r.name, 144 | Namespace: r.namespace, 145 | } 146 | } 147 | if r.sel != nil { 148 | result = SelectorFilter{ 149 | Next: result, 150 | Selector: r.sel, 151 | } 152 | } 153 | if r.fieldSelector != nil { 154 | result = FieldSelectorFilter{ 155 | Next: result, 156 | FieldSelector: r.fieldSelector, 157 | } 158 | } 159 | if r.includeFinalizing && !r.includeRemove && r.finalizeID == "" { 160 | result = IgnoreNilHandler{ 161 | Next: result, 162 | } 163 | } 164 | if !r.includeRemove && !r.includeFinalizing && r.finalizeID == "" { 165 | result = IgnoreRemoveHandler{ 166 | Next: result, 167 | } 168 | } 169 | 170 | if r.routeName != "" { 171 | result = ErrorPrefix{ 172 | prefix: "[" + r.routeName + "] ", 173 | Next: result, 174 | } 175 | } 176 | 177 | r.router.handlers.AddHandler(r.objType, result) 178 | } 179 | 180 | func (r *Router) Start(ctx context.Context) error { 181 | id, err := os.Hostname() 182 | if err != nil { 183 | return err 184 | } 185 | 186 | if r.hasHealthz { 187 | startHealthz(ctx) 188 | } 189 | 190 | r.handlers.onError = r.OnErrorHandler 191 | 192 | // It's OK to start the electionConfig even if it's nil. 193 | return r.electionConfig.Run(ctx, id, r.startHandlers, func(leader string) { 194 | // I am not the leader, so I am healthy until my controllers are started. 195 | if r.hasHealthz { 196 | setHealthy(r.name, id != leader) 197 | } 198 | }) 199 | } 200 | 201 | // startHandlers gets called when we become the leader or if there is no leader election. 202 | func (r *Router) startHandlers(ctx context.Context) error { 203 | var err error 204 | // This is the leader now, so not ready until the controller is started and caches are ready. 205 | if r.hasHealthz { 206 | setHealthy(r.name, false) 207 | defer setHealthy(r.name, err == nil) 208 | } 209 | 210 | err = r.handlers.Start(ctx) 211 | 212 | return err 213 | } 214 | 215 | func (r *Router) Handle(objType kclient.Object, h Handler) { 216 | r.routeName = name() 217 | r.RouteBuilder.Type(objType).Handler(h) 218 | } 219 | 220 | func (r *Router) HandleFunc(objType kclient.Object, h HandlerFunc) { 221 | r.routeName = name() 222 | r.RouteBuilder.Type(objType).Handler(h) 223 | } 224 | 225 | type IgnoreNilHandler struct { 226 | Next Handler 227 | } 228 | 229 | func (i IgnoreNilHandler) Handle(req Request, resp Response) error { 230 | if req.Object == nil { 231 | return nil 232 | } 233 | return i.Next.Handle(req, resp) 234 | } 235 | 236 | type IgnoreRemoveHandler struct { 237 | Next Handler 238 | } 239 | 240 | func (i IgnoreRemoveHandler) Handle(req Request, resp Response) error { 241 | if req.Object == nil || !req.Object.GetDeletionTimestamp().IsZero() { 242 | return nil 243 | } 244 | return i.Next.Handle(req, resp) 245 | } 246 | 247 | type ErrorPrefix struct { 248 | prefix string 249 | Next Handler 250 | } 251 | 252 | type errorPrefix struct { 253 | Prefix string 254 | Err error 255 | } 256 | 257 | func (e errorPrefix) Error() string { 258 | return e.Prefix + e.Err.Error() 259 | } 260 | 261 | func (e errorPrefix) Unwrap() error { 262 | return e.Err 263 | } 264 | 265 | func (e ErrorPrefix) Handle(req Request, resp Response) error { 266 | err := e.Next.Handle(req, resp) 267 | if err == nil { 268 | return nil 269 | } 270 | return errorPrefix{ 271 | Prefix: e.prefix, 272 | Err: err, 273 | } 274 | } 275 | 276 | type NameNamespaceFilter struct { 277 | Next Handler 278 | Name string 279 | Namespace string 280 | } 281 | 282 | func (n NameNamespaceFilter) Handle(req Request, resp Response) error { 283 | if n.Name != "" && req.Name != n.Name { 284 | return nil 285 | } 286 | if n.Namespace != "" && req.Namespace != n.Namespace { 287 | return nil 288 | } 289 | return n.Next.Handle(req, resp) 290 | } 291 | 292 | type SelectorFilter struct { 293 | Next Handler 294 | Selector labels.Selector 295 | } 296 | 297 | func (s SelectorFilter) Handle(req Request, resp Response) error { 298 | if req.Object == nil || !s.Selector.Matches(labels.Set(req.Object.GetLabels())) { 299 | return nil 300 | } 301 | return s.Next.Handle(req, resp) 302 | } 303 | 304 | type FieldSelectorFilter struct { 305 | Next Handler 306 | FieldSelector fields.Selector 307 | } 308 | 309 | func (s FieldSelectorFilter) Handle(req Request, resp Response) error { 310 | if req.Object == nil { 311 | return nil 312 | } 313 | if i, ok := req.Object.(fields.Fields); ok && s.FieldSelector.Matches(i) { 314 | return s.Next.Handle(req, resp) 315 | } 316 | return nil 317 | } 318 | -------------------------------------------------------------------------------- /pkg/router/save.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/acorn-io/baaah/pkg/apply" 7 | "github.com/acorn-io/baaah/pkg/backend" 8 | "k8s.io/apimachinery/pkg/api/equality" 9 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | "k8s.io/apimachinery/pkg/runtime/schema" 12 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 13 | ) 14 | 15 | type save struct { 16 | apply apply.Apply 17 | cache backend.CacheFactory 18 | client kclient.Client 19 | } 20 | 21 | func (s *save) save(unmodified runtime.Object, req Request, resp *response, watchingGVKS []schema.GroupVersionKind) (kclient.Object, error) { 22 | var owner = req.Object 23 | if owner == nil { 24 | owner := &unstructured.Unstructured{} 25 | owner.SetGroupVersionKind(req.GVK) 26 | owner.SetNamespace(req.Namespace) 27 | owner.SetName(req.Name) 28 | } 29 | apply := s.apply. 30 | WithPruneGVKs(watchingGVKS...) 31 | 32 | // Special case the situation where there are no objects and a retry later is set. 33 | // In this situation don't purge all the objects previously created 34 | if resp.noPrune || len(resp.objects) == 0 && resp.delay > 0 { 35 | apply = apply.WithNoPrune() 36 | } 37 | if err := apply.Apply(req.Ctx, owner, resp.objects...); err != nil { 38 | return nil, err 39 | } 40 | 41 | newObj := req.Object 42 | if newObj != nil && StatusChanged(unmodified, newObj) { 43 | if unmodObj, ok := unmodified.(kclient.Object); ok && unmodObj.GetGeneration() != newObj.GetGeneration() { 44 | if err := req.Get(unmodObj, unmodObj.GetNamespace(), unmodObj.GetName()); err != nil { 45 | return nil, err 46 | } 47 | unmodified = unmodObj 48 | if !StatusChanged(unmodified, newObj) { 49 | return newObj, nil 50 | } 51 | } 52 | return newObj, s.client.Status().Update(req.Ctx, newObj) 53 | } 54 | 55 | return newObj, nil 56 | } 57 | 58 | func statusField(obj runtime.Object) interface{} { 59 | v := reflect.ValueOf(obj).Elem() 60 | fieldValue := v.FieldByName("Status") 61 | if fieldValue.Kind() == reflect.Invalid { 62 | return nil 63 | } 64 | return fieldValue.Interface() 65 | } 66 | 67 | func StatusChanged(unmodified, newObj runtime.Object) bool { 68 | return !equality.Semantic.DeepEqual(statusField(unmodified), statusField(newObj)) 69 | } 70 | -------------------------------------------------------------------------------- /pkg/router/tester/generate.go: -------------------------------------------------------------------------------- 1 | package tester 2 | 3 | import ( 4 | "encoding/json" 5 | "math/rand" 6 | ) 7 | 8 | const ( 9 | characters = "bcdfghjklmnpqrstvwxz2456789" 10 | tokenLength = 54 11 | ) 12 | 13 | func generate(obj any) (string, error) { 14 | d, err := json.Marshal(obj) 15 | if err != nil { 16 | return "", err 17 | } 18 | r := rand.New(rand.NewSource(int64(len(d)))) 19 | token := make([]byte, tokenLength) 20 | for i := range token { 21 | token[i] = characters[r.Int63n(int64(len(characters)))] 22 | } 23 | return string(token), nil 24 | } 25 | -------------------------------------------------------------------------------- /pkg/router/tester/tester.go: -------------------------------------------------------------------------------- 1 | package tester 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "encoding/json" 8 | "fmt" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | "testing" 13 | "time" 14 | 15 | "github.com/acorn-io/baaah/pkg/router" 16 | "github.com/acorn-io/baaah/pkg/yaml" 17 | "github.com/hexops/autogold/v2" 18 | "github.com/stretchr/testify/assert" 19 | "github.com/stretchr/testify/require" 20 | "k8s.io/apimachinery/pkg/runtime" 21 | "k8s.io/apimachinery/pkg/runtime/schema" 22 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 23 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 24 | yaml2 "sigs.k8s.io/yaml" 25 | ) 26 | 27 | type Harness struct { 28 | Scheme *runtime.Scheme 29 | Existing []kclient.Object 30 | ExpectedOutput []kclient.Object 31 | ExpectedGoldenPath string 32 | ExpectedDelay time.Duration 33 | } 34 | 35 | func genericToTyped(scheme *runtime.Scheme, objs []runtime.Object) ([]kclient.Object, error) { 36 | result := make([]kclient.Object, 0, len(objs)) 37 | for _, obj := range objs { 38 | typedObj, err := scheme.New(obj.GetObjectKind().GroupVersionKind()) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | bytes, err := json.Marshal(obj) 44 | if err != nil { 45 | return nil, err 46 | } 47 | if err := json.Unmarshal(bytes, typedObj); err != nil { 48 | return nil, err 49 | } 50 | result = append(result, typedObj.(kclient.Object)) 51 | } 52 | return result, nil 53 | } 54 | 55 | func readFile(scheme *runtime.Scheme, dir, file string) ([]kclient.Object, error) { 56 | var ( 57 | path = filepath.Join(dir, file) 58 | objs []kclient.Object 59 | ) 60 | 61 | files, err := os.ReadDir(path + ".d") 62 | if os.IsNotExist(err) { 63 | } else if err != nil { 64 | return nil, err 65 | } 66 | 67 | for _, file := range files { 68 | if file.IsDir() || !strings.HasSuffix(file.Name(), ".yaml") { 69 | continue 70 | } 71 | nestedObj, err := readFile(scheme, path+".d", file.Name()) 72 | if err != nil { 73 | return nil, err 74 | } 75 | objs = append(objs, nestedObj...) 76 | } 77 | 78 | data, err := os.ReadFile(path) 79 | if os.IsNotExist(err) { 80 | return objs, nil 81 | } else if err != nil { 82 | return nil, fmt.Errorf("reading %s: %w", path, err) 83 | } 84 | newObjects, err := yaml.ToObjects(bytes.NewBuffer(data)) 85 | if err != nil { 86 | return nil, fmt.Errorf("unmarshalling %s: %w", path, err) 87 | } 88 | typedObjects, err := genericToTyped(scheme, newObjects) 89 | if err != nil { 90 | return nil, err 91 | } 92 | return append(objs, typedObjects...), nil 93 | } 94 | 95 | func FromDir(scheme *runtime.Scheme, path string) (*Harness, kclient.Object, error) { 96 | input, err := readFile(scheme, path, "input.yaml") 97 | if err != nil { 98 | return nil, nil, err 99 | } 100 | 101 | if len(input) != 1 { 102 | return nil, nil, fmt.Errorf("%s/%s does not include one input object", path, "input.yaml") 103 | } 104 | 105 | existing, err := readFile(scheme, path, "existing.yaml") 106 | if err != nil { 107 | return nil, nil, err 108 | } 109 | 110 | expected, err := readFile(scheme, path, "expected.yaml") 111 | if err != nil { 112 | return nil, nil, err 113 | } 114 | 115 | goldDir := path 116 | _, err = os.Stat(filepath.Join(goldDir, "expected.golden")) 117 | if os.IsNotExist(err) { 118 | if len(expected) > 0 { 119 | goldDir = "" 120 | } 121 | } else if err != nil { 122 | return nil, nil, err 123 | } 124 | 125 | return &Harness{ 126 | Scheme: scheme, 127 | Existing: existing, 128 | ExpectedOutput: expected, 129 | ExpectedGoldenPath: goldDir, 130 | ExpectedDelay: 0, 131 | }, input[0], nil 132 | } 133 | 134 | func DefaultTest(t *testing.T, scheme *runtime.Scheme, path string, handler router.HandlerFunc) (result *Response) { 135 | t.Helper() 136 | t.Run(path, func(t *testing.T) { 137 | harness, input, err := FromDir(scheme, path) 138 | if err != nil { 139 | t.Fatal(err) 140 | } 141 | result, err = harness.Invoke(t, input, handler) 142 | if err != nil { 143 | t.Fatal(err) 144 | } 145 | }) 146 | return 147 | } 148 | 149 | func (b *Harness) InvokeFunc(t *testing.T, input kclient.Object, handler router.HandlerFunc) (*Response, error) { 150 | t.Helper() 151 | return b.Invoke(t, input, handler) 152 | } 153 | 154 | func (b *Harness) InvokeFuncWithContext(t *testing.T, ctx context.Context, input kclient.Object, handler router.HandlerFunc) (*Response, error) { 155 | t.Helper() 156 | return b.InvokeWithContext(t, ctx, input, handler) 157 | } 158 | 159 | func NewRequest(t *testing.T, scheme *runtime.Scheme, input kclient.Object, existing ...kclient.Object) router.Request { 160 | t.Helper() 161 | return NewRequestWithContext(t, context.TODO(), scheme, input, existing...) 162 | } 163 | 164 | func NewRequestWithContext(t *testing.T, ctx context.Context, scheme *runtime.Scheme, input kclient.Object, existing ...kclient.Object) router.Request { 165 | t.Helper() 166 | gvk, err := apiutil.GVKForObject(input, scheme) 167 | if err != nil { 168 | t.Fatal(err) 169 | } 170 | 171 | return router.Request{ 172 | Client: &Client{ 173 | Objects: append(existing, input.DeepCopyObject().(kclient.Object)), 174 | SchemeObj: scheme, 175 | }, 176 | Object: input, 177 | Ctx: ctx, 178 | GVK: gvk, 179 | Namespace: input.GetNamespace(), 180 | Name: input.GetName(), 181 | Key: toKey(input.GetNamespace(), input.GetName()), 182 | FromTrigger: false, 183 | } 184 | } 185 | 186 | func (b *Harness) Invoke(t *testing.T, input kclient.Object, handler router.Handler) (*Response, error) { 187 | t.Helper() 188 | return b.InvokeWithContext(t, context.TODO(), input, handler) 189 | } 190 | 191 | func (b *Harness) InvokeWithContext(t *testing.T, ctx context.Context, input kclient.Object, handler router.Handler) (*Response, error) { 192 | t.Helper() 193 | var ( 194 | req = NewRequestWithContext(t, ctx, b.Scheme, input, b.Existing...) 195 | resp = Response{ 196 | Client: req.Client.(*Client), 197 | } 198 | ) 199 | 200 | err := handler.Handle(req, &resp) 201 | if err != nil { 202 | return &resp, err 203 | } 204 | 205 | expected, err := toObjectMap(b.Scheme, b.ExpectedOutput) 206 | if err != nil { 207 | return &resp, err 208 | } 209 | 210 | collected, err := toObjectMap(b.Scheme, resp.Collected) 211 | if err != nil { 212 | return &resp, err 213 | } 214 | 215 | assert.Equal(t, b.ExpectedDelay, resp.Delay) 216 | 217 | if b.ExpectedGoldenPath != "" { 218 | yamls := b.SanitizedYAML(t, resp.Collected) 219 | autogold.ExpectFile(t, yamls, autogold.Dir(b.ExpectedGoldenPath), autogold.Name("expected")) 220 | } 221 | 222 | if len(b.ExpectedOutput) == 0 { 223 | return &resp, nil 224 | } 225 | 226 | expectedKeys := map[ObjectKey]bool{} 227 | for k := range expected { 228 | expectedKeys[k] = true 229 | } 230 | collectedKeys := map[ObjectKey]bool{} 231 | for k := range collected { 232 | collectedKeys[k] = true 233 | } 234 | 235 | for key := range collectedKeys { 236 | assert.Containsf(t, expectedKeys, key, "Unexpected object %s/%s: %v", key.Namespace, key.Name, key.GVK) 237 | } 238 | 239 | for key := range expectedKeys { 240 | assert.Containsf(t, collectedKeys, key, "Missing expected object %s/%s: %v", key.Namespace, key.Name, key.GVK) 241 | if !assert.ObjectsAreEqual(expected[key], collected[key]) { 242 | left, _ := yaml2.Marshal(expected[key]) 243 | right, _ := yaml2.Marshal(collected[key]) 244 | 245 | left = stripLastTransition(left) 246 | right = stripLastTransition(right) 247 | assert.Equal(t, string(left), string(right), "object %s/%s (%v) does not match", key.Namespace, key.Name, key.GVK) 248 | } 249 | } 250 | 251 | return &resp, nil 252 | } 253 | 254 | func (b *Harness) SanitizedYAML(t *testing.T, objs []kclient.Object) string { 255 | t.Helper() 256 | var yamls []string 257 | for _, o := range objs { 258 | gvk, err := apiutil.GVKForObject(o, b.Scheme) 259 | require.NoError(t, err) 260 | o.GetObjectKind().SetGroupVersionKind(gvk) 261 | left, _ := yaml2.Marshal(o) 262 | 263 | left = stripLastTransition(left) 264 | yamls = append(yamls, string(left)) 265 | } 266 | return strings.Join(yamls, "\n---\n") 267 | } 268 | 269 | func stripLastTransition(buf []byte) []byte { 270 | result := &bytes.Buffer{} 271 | s := bufio.NewScanner(bytes.NewReader(buf)) 272 | for s.Scan() { 273 | line := s.Text() 274 | if strings.Contains(line, "lastTransitionTime:") { 275 | continue 276 | } 277 | result.WriteString(line) 278 | result.WriteString("\n") 279 | } 280 | return result.Bytes() 281 | } 282 | 283 | func toObjectMap(scheme *runtime.Scheme, objs []kclient.Object) (map[ObjectKey]kclient.Object, error) { 284 | result := map[ObjectKey]kclient.Object{} 285 | for _, o := range objs { 286 | gvk, err := apiutil.GVKForObject(o, scheme) 287 | if err != nil { 288 | return nil, err 289 | } 290 | o.GetObjectKind().SetGroupVersionKind(gvk) 291 | result[ObjectKey{ 292 | GVK: gvk, 293 | Namespace: o.GetNamespace(), 294 | Name: o.GetName(), 295 | }] = o 296 | } 297 | return result, nil 298 | } 299 | 300 | type ObjectKey struct { 301 | GVK schema.GroupVersionKind 302 | Namespace string 303 | Name string 304 | } 305 | 306 | func toKey(ns, name string) string { 307 | if ns == "" { 308 | return name 309 | } 310 | return ns + "/" + name 311 | } 312 | -------------------------------------------------------------------------------- /pkg/router/tester/types.go: -------------------------------------------------------------------------------- 1 | package tester 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | "time" 9 | 10 | "github.com/acorn-io/baaah/pkg/router" 11 | "github.com/acorn-io/baaah/pkg/uncached" 12 | "github.com/google/uuid" 13 | "golang.org/x/exp/maps" 14 | "k8s.io/apimachinery/pkg/api/errors" 15 | meta2 "k8s.io/apimachinery/pkg/api/meta" 16 | "k8s.io/apimachinery/pkg/labels" 17 | "k8s.io/apimachinery/pkg/runtime" 18 | "k8s.io/apimachinery/pkg/runtime/schema" 19 | "k8s.io/apimachinery/pkg/types" 20 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 21 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 22 | ) 23 | 24 | type Client struct { 25 | Objects []kclient.Object 26 | SchemeObj *runtime.Scheme 27 | Created []kclient.Object 28 | Updated []kclient.Object 29 | } 30 | 31 | func (c Client) objects() []kclient.Object { 32 | return append(append(c.Objects, c.Created...), c.Updated...) 33 | } 34 | 35 | func (c *Client) Get(ctx context.Context, key kclient.ObjectKey, out kclient.Object, opts ...kclient.GetOption) error { 36 | if u, ok := out.(*uncached.Holder); ok { 37 | out = u.Object 38 | } 39 | t := reflect.TypeOf(out) 40 | var ns string 41 | if key.Namespace != "" { 42 | ns = key.Namespace 43 | } 44 | 45 | // iterate over the slice in reverse because updated objects are at the end. created objects are before them. 46 | // in the scenario where an object has been created and then updated, or updated twice, we want the last object in 47 | objs := c.objects() 48 | for i := len(objs) - 1; i >= 0; i-- { 49 | obj := objs[i] 50 | if reflect.TypeOf(obj) != t { 51 | continue 52 | } 53 | if obj.GetName() == key.Name && 54 | obj.GetNamespace() == ns { 55 | copy(out, obj) 56 | return nil 57 | } 58 | } 59 | return errors.NewNotFound(schema.GroupResource{ 60 | Group: fmt.Sprintf("Unknown group from test: %T", out), 61 | Resource: fmt.Sprintf("Unknown resource from test: %T", out), 62 | }, key.Name) 63 | } 64 | 65 | func copy(dest, src kclient.Object) { 66 | srcCopy := src.DeepCopyObject() 67 | reflect.Indirect(reflect.ValueOf(dest)).Set(reflect.Indirect(reflect.ValueOf(srcCopy))) 68 | } 69 | 70 | func (c *Client) List(ctx context.Context, objList kclient.ObjectList, opts ...kclient.ListOption) error { 71 | if u, ok := objList.(*uncached.HolderList); ok { 72 | objList = u.ObjectList 73 | } 74 | 75 | listOpts := &kclient.ListOptions{} 76 | for _, opt := range opts { 77 | opt.ApplyToList(listOpts) 78 | } 79 | 80 | gvk, err := apiutil.GVKForObject(objList, c.SchemeObj) 81 | if err != nil { 82 | return err 83 | } 84 | if !strings.HasSuffix(gvk.Kind, "List") { 85 | return fmt.Errorf("invalid list object %v, Kind must end with List", gvk) 86 | } 87 | gvk.Kind = strings.TrimSuffix(gvk.Kind, "List") 88 | genericObj, err := c.SchemeObj.New(gvk) 89 | if err != nil { 90 | return err 91 | } 92 | obj := genericObj.(kclient.Object) 93 | t := reflect.TypeOf(obj) 94 | var ns string 95 | if listOpts.Namespace != "" { 96 | ns = listOpts.Namespace 97 | } 98 | 99 | // put objects into a map because c.objects() returns both created and updated objects, with updates coming after 100 | // created. this will ensure the last object in is what is returned 101 | resultObjs := make(map[string]runtime.Object) 102 | for _, testObj := range c.objects() { 103 | if testObj.GetNamespace() != ns { 104 | continue 105 | } 106 | if reflect.TypeOf(testObj) != t { 107 | continue 108 | } 109 | if opts != nil && listOpts.LabelSelector != nil && !listOpts.LabelSelector.Matches(labels.Set(testObj.GetLabels())) { 110 | continue 111 | } 112 | copy(obj, testObj) 113 | resultObjs[testObj.GetNamespace()+"/"+testObj.GetName()] = testObj 114 | newObj, err := c.SchemeObj.New(gvk) 115 | if err != nil { 116 | return err 117 | } 118 | obj = newObj.(kclient.Object) 119 | } 120 | return meta2.SetList(objList, maps.Values(resultObjs)) 121 | } 122 | 123 | func (c *Client) Create(ctx context.Context, obj kclient.Object, opts ...kclient.CreateOption) error { 124 | obj.SetUID(types.UID(uuid.New().String())) 125 | if obj.GetName() == "" && obj.GetGenerateName() != "" { 126 | r, err := generate(obj) 127 | if err != nil { 128 | return err 129 | } 130 | obj.SetName(obj.GetGenerateName() + r[:5]) 131 | } 132 | c.Created = append(c.Created, obj) 133 | return nil 134 | } 135 | 136 | func (c *Client) Update(ctx context.Context, o kclient.Object, opts ...kclient.UpdateOption) error { 137 | t := reflect.TypeOf(o) 138 | 139 | for _, obj := range c.objects() { 140 | if reflect.TypeOf(obj) != t { 141 | continue 142 | } 143 | if obj.GetName() == o.GetName() && obj.GetNamespace() == o.GetNamespace() { 144 | c.Updated = append(c.Updated, o) 145 | return nil 146 | } 147 | } 148 | 149 | return errors.NewNotFound(schema.GroupResource{ 150 | Group: fmt.Sprintf("Unknown group from test: %T", o), 151 | Resource: fmt.Sprintf("Unknown resource from test: %T", o), 152 | }, o.GetName()) 153 | } 154 | 155 | type Response struct { 156 | router.ResponseAttributes 157 | 158 | Delay time.Duration 159 | Collected []kclient.Object 160 | Client *Client 161 | NoPrune bool 162 | } 163 | 164 | func (r *Response) DisablePrune() { 165 | r.NoPrune = true 166 | } 167 | 168 | func (r *Response) RetryAfter(delay time.Duration) { 169 | if r.Delay == 0 || delay < r.Delay { 170 | r.Delay = delay 171 | } 172 | } 173 | 174 | func (r *Response) Objects(obj ...kclient.Object) { 175 | r.Collected = append(r.Collected, obj...) 176 | } 177 | 178 | func (c *Client) Delete(ctx context.Context, obj kclient.Object, opts ...kclient.DeleteOption) error { 179 | //TODO implement me 180 | panic("implement me") 181 | } 182 | 183 | func (c *Client) Patch(ctx context.Context, obj kclient.Object, patch kclient.Patch, opts ...kclient.PatchOption) error { 184 | //TODO implement me 185 | panic("implement me") 186 | } 187 | 188 | func (c *Client) DeleteAllOf(ctx context.Context, obj kclient.Object, opts ...kclient.DeleteAllOfOption) error { 189 | //TODO implement me 190 | panic("implement me") 191 | } 192 | 193 | func (c *Client) Status() kclient.StatusWriter { 194 | //TODO implement me 195 | panic("implement me") 196 | } 197 | 198 | func (c *Client) SubResource(subResource string) kclient.SubResourceClient { 199 | //TODO implement me 200 | panic("implement me") 201 | } 202 | 203 | func (c *Client) Scheme() *runtime.Scheme { 204 | return c.SchemeObj 205 | } 206 | 207 | func (c *Client) RESTMapper() meta2.RESTMapper { 208 | //TODO implement me 209 | panic("implement me") 210 | } 211 | 212 | func (c *Client) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) { 213 | return apiutil.GVKForObject(obj, c.SchemeObj) 214 | } 215 | 216 | func (c *Client) IsObjectNamespaced(obj runtime.Object) (bool, error) { 217 | //TODO implement me 218 | panic("implement me") 219 | } 220 | -------------------------------------------------------------------------------- /pkg/router/trigger.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | 7 | "github.com/acorn-io/baaah/pkg/backend" 8 | "github.com/acorn-io/baaah/pkg/log" 9 | "github.com/acorn-io/baaah/pkg/uncached" 10 | "k8s.io/apimachinery/pkg/fields" 11 | "k8s.io/apimachinery/pkg/labels" 12 | "k8s.io/apimachinery/pkg/runtime" 13 | "k8s.io/apimachinery/pkg/runtime/schema" 14 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 15 | ) 16 | 17 | type triggers struct { 18 | lock sync.RWMutex 19 | matchers map[schema.GroupVersionKind]map[enqueueTarget][]objectMatcher 20 | trigger backend.Trigger 21 | gvkLookup backend.Backend 22 | scheme *runtime.Scheme 23 | watcher watcher 24 | } 25 | 26 | type watcher interface { 27 | WatchGVK(gvks ...schema.GroupVersionKind) error 28 | } 29 | 30 | type enqueueTarget struct { 31 | key string 32 | gvk schema.GroupVersionKind 33 | } 34 | 35 | func (m *triggers) invokeTriggers(req Request) { 36 | m.lock.RLock() 37 | defer m.lock.RUnlock() 38 | 39 | for enqueueTarget, matchers := range m.matchers[req.GVK] { 40 | if enqueueTarget.gvk == req.GVK && 41 | enqueueTarget.key == req.Key { 42 | continue 43 | } 44 | for _, matcher := range matchers { 45 | if matcher.Match(req.Namespace, req.Name, req.Object) { 46 | log.Debugf("Triggering [%s] [%v] from [%s] [%v]", enqueueTarget.key, enqueueTarget.gvk, req.Key, req.GVK) 47 | _ = m.trigger.Trigger(enqueueTarget.gvk, enqueueTarget.key, 0) 48 | break 49 | } 50 | } 51 | } 52 | } 53 | 54 | func (m *triggers) register(gvk schema.GroupVersionKind, key string, targetGVK schema.GroupVersionKind, mr objectMatcher) { 55 | m.lock.Lock() 56 | defer m.lock.Unlock() 57 | 58 | target := enqueueTarget{ 59 | key: key, 60 | gvk: gvk, 61 | } 62 | matchers, ok := m.matchers[targetGVK] 63 | if !ok { 64 | matchers = map[enqueueTarget][]objectMatcher{} 65 | m.matchers[targetGVK] = matchers 66 | } 67 | for _, existing := range matchers[target] { 68 | if existing.Equals(mr) { 69 | return 70 | } 71 | } 72 | matchers[target] = append(matchers[target], mr) 73 | } 74 | 75 | func (m *triggers) Trigger(req Request) { 76 | if !req.FromTrigger { 77 | m.invokeTriggers(req) 78 | } 79 | } 80 | 81 | func (m *triggers) Register(sourceGVK schema.GroupVersionKind, key string, obj runtime.Object, namespace, name string, selector labels.Selector, fields fields.Selector) (schema.GroupVersionKind, bool, error) { 82 | if uncached.IsWrapped(obj) { 83 | return schema.GroupVersionKind{}, false, nil 84 | } 85 | gvk, err := m.gvkLookup.GVKForObject(obj, m.scheme) 86 | if err != nil { 87 | return gvk, false, err 88 | } 89 | 90 | if _, ok := obj.(kclient.ObjectList); ok { 91 | gvk.Kind = strings.TrimSuffix(gvk.Kind, "List") 92 | } 93 | 94 | m.register(sourceGVK, key, gvk, objectMatcher{ 95 | Namespace: namespace, 96 | Name: name, 97 | Selector: selector, 98 | Fields: fields, 99 | }) 100 | 101 | return gvk, true, m.watcher.WatchGVK(gvk) 102 | } 103 | 104 | // UnregisterAndTrigger will unregister all triggers for the object, both as source and target. 105 | // If a trigger source matches the object exactly, then the trigger will be invoked. 106 | func (m *triggers) UnregisterAndTrigger(req Request) { 107 | m.lock.Lock() 108 | defer m.lock.Unlock() 109 | 110 | remainingMatchers := map[schema.GroupVersionKind]map[enqueueTarget][]objectMatcher{} 111 | 112 | for targetGVK, matchers := range m.matchers { 113 | for target, mts := range matchers { 114 | if target.gvk == req.GVK && target.key == req.Key { 115 | // If the target is the GVK and key we are unregistering, then skip it 116 | continue 117 | } 118 | for _, mt := range mts { 119 | if targetGVK != req.GVK || mt.Namespace != req.Namespace || mt.Name != req.Name { 120 | // If the matcher matches the deleted object exactly, then skip the matcher. 121 | if remainingMatchers[targetGVK] == nil { 122 | remainingMatchers[targetGVK] = make(map[enqueueTarget][]objectMatcher) 123 | } 124 | remainingMatchers[targetGVK][target] = append(remainingMatchers[targetGVK][target], mt) 125 | } 126 | if targetGVK == req.GVK && mt.Match(req.Namespace, req.Name, req.Object) { 127 | log.Debugf("Triggering [%s] [%v] from [%s] [%v] on delete", target.key, target.gvk, req.Key, req.GVK) 128 | _ = m.trigger.Trigger(target.gvk, target.key, 0) 129 | } 130 | } 131 | } 132 | } 133 | 134 | m.matchers = remainingMatchers 135 | } 136 | -------------------------------------------------------------------------------- /pkg/router/types.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | apierrors "k8s.io/apimachinery/pkg/api/errors" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "k8s.io/apimachinery/pkg/runtime/schema" 10 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 11 | ) 12 | 13 | type AddToSchemer func(s *runtime.Scheme) error 14 | 15 | type Handler interface { 16 | Handle(req Request, resp Response) error 17 | } 18 | 19 | type Middleware func(h Handler) Handler 20 | 21 | type HandlerFunc func(req Request, resp Response) error 22 | 23 | // ErrorHandler is a user defined function to handle an error. If the 24 | // ErrorHandler returns nil this req is considered handled and will not 25 | // be re-enqueued. If a non-nil resp is return this key will be 26 | // re-enqueued. ErrorHandler will be call for nil errors also so 27 | // That the ErrorHandler can possibly clear a previous error state. 28 | type ErrorHandler func(req Request, resp Response, err error) error 29 | 30 | func (h HandlerFunc) Handle(req Request, resp Response) error { 31 | return h(req, resp) 32 | } 33 | 34 | type Request struct { 35 | Client kclient.WithWatch 36 | Object kclient.Object 37 | Ctx context.Context 38 | GVK schema.GroupVersionKind 39 | Namespace string 40 | Name string 41 | Key string 42 | FromTrigger bool 43 | } 44 | 45 | func (r *Request) WithContext(ctx context.Context) Request { 46 | newRequest := *r 47 | newRequest.Ctx = ctx 48 | return newRequest 49 | } 50 | 51 | func (r *Request) List(object kclient.ObjectList, opts *kclient.ListOptions) error { 52 | return r.Client.List(r.Ctx, object, opts) 53 | } 54 | 55 | func (r *Request) Get(object kclient.Object, namespace, name string) error { 56 | return r.Client.Get(r.Ctx, Key(namespace, name), object) 57 | } 58 | 59 | func (r *Request) Delete(object kclient.Object) error { 60 | err := r.Client.Delete(r.Ctx, object) 61 | if apierrors.IsNotFound(err) { 62 | return nil 63 | } 64 | return err 65 | } 66 | 67 | type Response interface { 68 | Attributes() map[string]any 69 | DisablePrune() 70 | RetryAfter(delay time.Duration) 71 | Objects(obj ...kclient.Object) 72 | } 73 | 74 | func Key(namespace, name string) kclient.ObjectKey { 75 | return kclient.ObjectKey{ 76 | Name: name, 77 | Namespace: namespace, 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /pkg/runtime/backend.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "strconv" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | "github.com/acorn-io/baaah/pkg/backend" 13 | "github.com/acorn-io/baaah/pkg/fields" 14 | "github.com/acorn-io/baaah/pkg/router" 15 | "github.com/acorn-io/baaah/pkg/uncached" 16 | "k8s.io/apimachinery/pkg/runtime" 17 | "k8s.io/apimachinery/pkg/runtime/schema" 18 | kcache "k8s.io/client-go/tools/cache" 19 | "sigs.k8s.io/controller-runtime/pkg/cache" 20 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 21 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 22 | ) 23 | 24 | var DefaultThreadiness = 5 25 | 26 | func init() { 27 | i, _ := strconv.Atoi(os.Getenv("BAAAH_THREADINESS")) 28 | if i > 0 { 29 | DefaultThreadiness = i 30 | } 31 | } 32 | 33 | type Backend struct { 34 | *cacheClient 35 | 36 | cacheFactory SharedControllerFactory 37 | cache cache.Cache 38 | startedLock *sync.RWMutex 39 | started bool 40 | } 41 | 42 | func newBackend(cacheFactory SharedControllerFactory, client *cacheClient, cache cache.Cache) *Backend { 43 | return &Backend{ 44 | cacheClient: client, 45 | cacheFactory: cacheFactory, 46 | cache: cache, 47 | startedLock: new(sync.RWMutex), 48 | } 49 | } 50 | 51 | func (b *Backend) Start(ctx context.Context) (err error) { 52 | b.startedLock.Lock() 53 | defer b.startedLock.Unlock() 54 | defer func() { 55 | if err == nil { 56 | b.started = true 57 | } 58 | }() 59 | if err := b.cacheFactory.Start(ctx, DefaultThreadiness); err != nil { 60 | return err 61 | } 62 | if !b.cache.WaitForCacheSync(ctx) { 63 | return fmt.Errorf("failed to wait for caches to sync") 64 | } 65 | if !b.started { 66 | b.cacheClient.startPurge(ctx) 67 | } 68 | 69 | return nil 70 | } 71 | 72 | func (b *Backend) Trigger(gvk schema.GroupVersionKind, key string, delay time.Duration) error { 73 | controller, err := b.cacheFactory.ForKind(gvk) 74 | if err != nil { 75 | return err 76 | } 77 | if delay > 0 { 78 | ns, name, ok := strings.Cut(key, "/") 79 | if ok { 80 | controller.EnqueueAfter(ns, name, delay) 81 | } else { 82 | controller.EnqueueAfter("", key, delay) 83 | } 84 | } else { 85 | controller.EnqueueKey(router.TriggerPrefix + key) 86 | } 87 | return nil 88 | } 89 | 90 | func (b *Backend) addIndexer(ctx context.Context, gvk schema.GroupVersionKind) error { 91 | obj, err := b.Scheme().New(gvk) 92 | if err != nil { 93 | return err 94 | } 95 | f, ok := obj.(fields.Fields) 96 | if !ok { 97 | return nil 98 | } 99 | 100 | cache, err := b.cache.GetInformerForKind(ctx, gvk) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | indexers := map[string]kcache.IndexFunc{} 106 | for _, field := range f.FieldNames() { 107 | field := field 108 | indexers["field:"+field] = func(obj interface{}) ([]string, error) { 109 | f, ok := obj.(fields.Fields) 110 | if !ok { 111 | return nil, nil 112 | } 113 | v := f.Get(field) 114 | if v == "" { 115 | return nil, nil 116 | } 117 | vals := []string{keyFunc("", v)} 118 | if ko, ok := obj.(kclient.Object); ok && ko.GetNamespace() != "" { 119 | vals = append(vals, keyFunc(ko.GetNamespace(), v)) 120 | } 121 | return vals, nil 122 | } 123 | } 124 | return cache.AddIndexers(indexers) 125 | } 126 | 127 | func (b *Backend) Watcher(ctx context.Context, gvk schema.GroupVersionKind, name string, cb backend.Callback) error { 128 | c, err := b.cacheFactory.ForKind(gvk) 129 | if err != nil { 130 | return err 131 | } 132 | if err := b.addIndexer(ctx, gvk); err != nil { 133 | return err 134 | } 135 | handler := SharedControllerHandlerFunc(func(key string, obj runtime.Object) (runtime.Object, error) { 136 | return cb(gvk, key, obj) 137 | }) 138 | if err := c.RegisterHandler(ctx, fmt.Sprintf("%s %v", name, gvk), handler); err != nil { 139 | return err 140 | } 141 | 142 | if b.hasStarted() { 143 | return c.Start(ctx, DefaultThreadiness) 144 | } 145 | return nil 146 | } 147 | 148 | func (b *Backend) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) { 149 | return b.uncached.GroupVersionKindFor(obj) 150 | } 151 | 152 | func (b *Backend) IsObjectNamespaced(obj runtime.Object) (bool, error) { 153 | return b.uncached.IsObjectNamespaced(obj) 154 | } 155 | 156 | func (b *Backend) GVKForObject(obj runtime.Object, scheme *runtime.Scheme) (schema.GroupVersionKind, error) { 157 | return apiutil.GVKForObject(uncached.Unwrap(obj), scheme) 158 | } 159 | 160 | func (b *Backend) IndexField(ctx context.Context, obj kclient.Object, field string, extractValue kclient.IndexerFunc) error { 161 | return b.cache.IndexField(ctx, obj, field, extractValue) 162 | } 163 | 164 | func (b *Backend) GetInformerForKind(ctx context.Context, gvk schema.GroupVersionKind) (kcache.SharedIndexInformer, error) { 165 | i, err := b.cache.GetInformerForKind(ctx, gvk) 166 | if err != nil { 167 | return nil, err 168 | } 169 | return i.(kcache.SharedIndexInformer), nil 170 | } 171 | 172 | func (b *Backend) hasStarted() bool { 173 | b.startedLock.RLock() 174 | defer b.startedLock.RUnlock() 175 | return b.started 176 | } 177 | -------------------------------------------------------------------------------- /pkg/runtime/cached.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "strconv" 7 | "sync" 8 | "time" 9 | 10 | "github.com/acorn-io/baaah/pkg/uncached" 11 | apierrors "k8s.io/apimachinery/pkg/api/errors" 12 | "k8s.io/apimachinery/pkg/api/meta" 13 | "k8s.io/apimachinery/pkg/runtime" 14 | "k8s.io/apimachinery/pkg/runtime/schema" 15 | "k8s.io/apimachinery/pkg/watch" 16 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 17 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 18 | ) 19 | 20 | const ( 21 | cacheDuration = 10 * time.Second 22 | ) 23 | 24 | type objectKey struct { 25 | gvk schema.GroupVersionKind 26 | namespace, name string 27 | } 28 | 29 | type objectValue struct { 30 | Object kclient.Object 31 | Inserted time.Time 32 | } 33 | 34 | type cacheClient struct { 35 | uncached kclient.WithWatch 36 | cached kclient.Client 37 | 38 | recent map[objectKey]objectValue 39 | recentLock sync.Mutex 40 | } 41 | 42 | func newer(oldRV, newRV string) bool { 43 | if len(oldRV) == len(newRV) { 44 | return oldRV < newRV 45 | } 46 | oldI, err := strconv.Atoi(oldRV) 47 | if err != nil { 48 | return true 49 | } 50 | newI, err := strconv.Atoi(newRV) 51 | if err != nil { 52 | return false 53 | } 54 | return oldI < newI 55 | } 56 | 57 | func newCacheClient(uncached kclient.WithWatch, cached kclient.Client) *cacheClient { 58 | return &cacheClient{ 59 | uncached: uncached, 60 | cached: cached, 61 | recent: map[objectKey]objectValue{}, 62 | } 63 | } 64 | 65 | func (c *cacheClient) startPurge(ctx context.Context) { 66 | go func() { 67 | for { 68 | select { 69 | case <-ctx.Done(): 70 | return 71 | case <-time.After(cacheDuration): 72 | } 73 | 74 | now := time.Now() 75 | c.recentLock.Lock() 76 | for k, v := range c.recent { 77 | if v.Inserted.Add(cacheDuration).Before(now) { 78 | delete(c.recent, k) 79 | } 80 | } 81 | c.recentLock.Unlock() 82 | } 83 | }() 84 | } 85 | 86 | func (c *cacheClient) deleteStore(obj kclient.Object) { 87 | gvk, err := apiutil.GVKForObject(obj, c.Scheme()) 88 | if err != nil { 89 | return 90 | } 91 | c.recentLock.Lock() 92 | delete(c.recent, objectKey{ 93 | gvk: gvk, 94 | namespace: obj.GetNamespace(), 95 | name: obj.GetName(), 96 | }) 97 | c.recentLock.Unlock() 98 | } 99 | 100 | func (c *cacheClient) store(obj kclient.Object) { 101 | gvk, err := apiutil.GVKForObject(obj, c.Scheme()) 102 | if err != nil { 103 | return 104 | } 105 | c.recentLock.Lock() 106 | c.recent[objectKey{ 107 | gvk: gvk, 108 | namespace: obj.GetNamespace(), 109 | name: obj.GetName(), 110 | }] = objectValue{ 111 | Object: obj.DeepCopyObject().(kclient.Object), 112 | Inserted: time.Now(), 113 | } 114 | c.recentLock.Unlock() 115 | } 116 | 117 | func (c *cacheClient) Get(ctx context.Context, key kclient.ObjectKey, obj kclient.Object, opts ...kclient.GetOption) error { 118 | if u, ok := obj.(*uncached.Holder); ok { 119 | return c.uncached.Get(ctx, key, u.Object, opts...) 120 | } 121 | 122 | getErr := c.cached.Get(ctx, key, obj) 123 | if getErr != nil && !apierrors.IsNotFound(getErr) { 124 | return getErr 125 | } 126 | 127 | gvk, err := apiutil.GVKForObject(obj, c.Scheme()) 128 | if err != nil { 129 | return errors.Join(getErr, err) 130 | } 131 | 132 | cacheKey := objectKey{ 133 | gvk: gvk, 134 | namespace: obj.GetNamespace(), 135 | name: obj.GetName(), 136 | } 137 | 138 | c.recentLock.Lock() 139 | cachedObj, ok := c.recent[cacheKey] 140 | c.recentLock.Unlock() 141 | 142 | if apierrors.IsNotFound(getErr) { 143 | if ok { 144 | return CopyInto(obj, cachedObj.Object) 145 | } else { 146 | return c.uncached.Get(ctx, key, obj, opts...) 147 | } 148 | } 149 | 150 | if ok && newer(obj.GetResourceVersion(), cachedObj.Object.GetResourceVersion()) { 151 | return CopyInto(obj, cachedObj.Object) 152 | } 153 | 154 | return nil 155 | } 156 | 157 | func (c *cacheClient) List(ctx context.Context, list kclient.ObjectList, opts ...kclient.ListOption) error { 158 | if u, ok := list.(*uncached.HolderList); ok { 159 | return c.uncached.List(ctx, u.ObjectList, opts...) 160 | } 161 | return c.cached.List(ctx, list, opts...) 162 | } 163 | 164 | func (c *cacheClient) Create(ctx context.Context, obj kclient.Object, opts ...kclient.CreateOption) error { 165 | if u, ok := obj.(*uncached.Holder); ok { 166 | return c.uncached.Create(ctx, u.Object, opts...) 167 | } 168 | err := c.cached.Create(ctx, obj, opts...) 169 | if err != nil { 170 | return err 171 | } 172 | c.store(obj) 173 | return nil 174 | } 175 | 176 | func (c *cacheClient) Delete(ctx context.Context, obj kclient.Object, opts ...kclient.DeleteOption) error { 177 | if u, ok := obj.(*uncached.Holder); ok { 178 | return c.uncached.Delete(ctx, u.Object, opts...) 179 | } 180 | err := c.cached.Delete(ctx, obj, opts...) 181 | if err != nil { 182 | return err 183 | } 184 | c.deleteStore(obj) 185 | return nil 186 | } 187 | 188 | func (c *cacheClient) Update(ctx context.Context, obj kclient.Object, opts ...kclient.UpdateOption) error { 189 | if u, ok := obj.(*uncached.Holder); ok { 190 | return c.uncached.Update(ctx, u.Object, opts...) 191 | } 192 | err := c.cached.Update(ctx, obj, opts...) 193 | if err != nil { 194 | return err 195 | } 196 | c.store(obj) 197 | return nil 198 | } 199 | 200 | func (c *cacheClient) Patch(ctx context.Context, obj kclient.Object, patch kclient.Patch, opts ...kclient.PatchOption) error { 201 | if u, ok := obj.(*uncached.Holder); ok { 202 | return c.uncached.Patch(ctx, u.Object, patch, opts...) 203 | } 204 | err := c.cached.Patch(ctx, obj, patch, opts...) 205 | if err != nil { 206 | return err 207 | } 208 | c.store(obj) 209 | return nil 210 | } 211 | 212 | func (c *cacheClient) DeleteAllOf(ctx context.Context, obj kclient.Object, opts ...kclient.DeleteAllOfOption) error { 213 | return c.cached.DeleteAllOf(ctx, obj, opts...) 214 | } 215 | 216 | func (c *cacheClient) SubResource(subResource string) kclient.SubResourceClient { 217 | client := c.cached.SubResource(subResource) 218 | return &subResourceClient{ 219 | c: c, 220 | reader: client, 221 | writer: client, 222 | } 223 | } 224 | 225 | func (c *cacheClient) Watch(ctx context.Context, obj kclient.ObjectList, opts ...kclient.ListOption) (watch.Interface, error) { 226 | return c.uncached.Watch(ctx, obj, opts...) 227 | } 228 | 229 | func (c *cacheClient) Status() kclient.StatusWriter { 230 | return &subResourceClient{ 231 | c: c, 232 | writer: c.cached.Status(), 233 | } 234 | } 235 | 236 | func (c *cacheClient) Scheme() *runtime.Scheme { 237 | return c.cached.Scheme() 238 | } 239 | 240 | func (c *cacheClient) RESTMapper() meta.RESTMapper { 241 | return c.cached.RESTMapper() 242 | } 243 | 244 | type subResourceClient struct { 245 | c *cacheClient 246 | writer kclient.SubResourceWriter 247 | reader kclient.SubResourceReader 248 | } 249 | 250 | func (s *subResourceClient) Get(ctx context.Context, obj kclient.Object, subResource kclient.Object, opts ...kclient.SubResourceGetOption) error { 251 | return s.reader.Get(ctx, uncached.Unwrap(obj).(kclient.Object), subResource, opts...) 252 | } 253 | 254 | func (s *subResourceClient) Create(ctx context.Context, obj kclient.Object, subResource kclient.Object, opts ...kclient.SubResourceCreateOption) error { 255 | return s.writer.Create(ctx, uncached.Unwrap(obj).(kclient.Object), subResource, opts...) 256 | } 257 | 258 | func (s *subResourceClient) Update(ctx context.Context, obj kclient.Object, opts ...kclient.SubResourceUpdateOption) error { 259 | err := s.writer.Update(ctx, uncached.Unwrap(obj).(kclient.Object), opts...) 260 | if err != nil { 261 | return err 262 | } 263 | s.c.store(obj) 264 | return nil 265 | } 266 | 267 | func (s *subResourceClient) Patch(ctx context.Context, obj kclient.Object, patch kclient.Patch, opts ...kclient.SubResourcePatchOption) error { 268 | err := s.writer.Patch(ctx, uncached.Unwrap(obj).(kclient.Object), patch, opts...) 269 | if err != nil { 270 | return err 271 | } 272 | s.c.store(obj) 273 | return nil 274 | } 275 | -------------------------------------------------------------------------------- /pkg/runtime/clients.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/acorn-io/baaah/pkg/mapper" 7 | "github.com/acorn-io/baaah/pkg/runtime/multi" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "k8s.io/client-go/rest" 10 | "k8s.io/client-go/util/workqueue" 11 | "sigs.k8s.io/controller-runtime/pkg/cache" 12 | "sigs.k8s.io/controller-runtime/pkg/client" 13 | ) 14 | 15 | type Runtime struct { 16 | Backend *Backend 17 | } 18 | 19 | type Config struct { 20 | Rest *rest.Config 21 | Namespace string 22 | } 23 | 24 | func NewRuntime(cfg *rest.Config, scheme *runtime.Scheme) (*Runtime, error) { 25 | return NewRuntimeWithConfig(Config{Rest: cfg}, scheme) 26 | } 27 | 28 | func NewRuntimeForNamespace(cfg *rest.Config, namespace string, scheme *runtime.Scheme) (*Runtime, error) { 29 | return NewRuntimeWithConfigs(Config{Rest: cfg, Namespace: namespace}, nil, scheme) 30 | } 31 | 32 | func NewRuntimeWithConfig(cfg Config, scheme *runtime.Scheme) (*Runtime, error) { 33 | return NewRuntimeWithConfigs(cfg, nil, scheme) 34 | } 35 | 36 | func NewRuntimeWithConfigs(defaultConfig Config, apiGroupConfigs map[string]Config, scheme *runtime.Scheme) (*Runtime, error) { 37 | clients := make(map[string]client.WithWatch, len(apiGroupConfigs)) 38 | cachedClients := make(map[string]client.Client, len(apiGroupConfigs)) 39 | caches := make(map[string]cache.Cache, len(apiGroupConfigs)) 40 | 41 | for key, cfg := range apiGroupConfigs { 42 | uncachedClient, cachedClient, theCache, err := getClients(cfg, scheme) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | clients[key] = uncachedClient 48 | caches[key] = theCache 49 | cachedClients[key] = cachedClient 50 | } 51 | 52 | uncachedClient, cachedClient, theCache, err := getClients(defaultConfig, scheme) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | aggUncachedClient := multi.NewWithWatch(uncachedClient, clients) 58 | aggCachedClient := multi.NewClient(cachedClient, cachedClients) 59 | aggCache := multi.NewCache(scheme, theCache, caches) 60 | 61 | factory := NewSharedControllerFactory(aggUncachedClient, aggCache, &SharedControllerFactoryOptions{ 62 | // In baaah this is only invoked when a key fails to process 63 | DefaultRateLimiter: workqueue.NewMaxOfRateLimiter( 64 | // This will go .5, 1, 2, 4, 8 seconds, etc up until 15 minutes 65 | workqueue.NewItemExponentialFailureRateLimiter(500*time.Millisecond, 15*time.Minute), 66 | ), 67 | }) 68 | 69 | return &Runtime{ 70 | Backend: newBackend(factory, newCacheClient(aggUncachedClient, aggCachedClient), aggCache), 71 | }, nil 72 | } 73 | 74 | func getClients(cfg Config, scheme *runtime.Scheme) (uncachedClient client.WithWatch, cachedClient client.Client, theCache cache.Cache, err error) { 75 | mapper, err := mapper.New(cfg.Rest) 76 | if err != nil { 77 | return nil, nil, nil, err 78 | } 79 | 80 | uncachedClient, err = client.NewWithWatch(cfg.Rest, client.Options{ 81 | Scheme: scheme, 82 | Mapper: mapper, 83 | }) 84 | if err != nil { 85 | return nil, nil, nil, err 86 | } 87 | 88 | var namespaces map[string]cache.Config 89 | if cfg.Namespace != "" { 90 | namespaces = map[string]cache.Config{} 91 | namespaces[cfg.Namespace] = cache.Config{} 92 | } 93 | 94 | theCache, err = cache.New(cfg.Rest, cache.Options{ 95 | Mapper: mapper, 96 | Scheme: scheme, 97 | DefaultNamespaces: namespaces, 98 | }) 99 | if err != nil { 100 | return nil, nil, nil, err 101 | } 102 | 103 | cachedClient, err = client.New(cfg.Rest, client.Options{ 104 | Scheme: scheme, 105 | Mapper: mapper, 106 | Cache: &client.CacheOptions{ 107 | Reader: theCache, 108 | }, 109 | }) 110 | if err != nil { 111 | return nil, nil, nil, err 112 | } 113 | 114 | return uncachedClient, cachedClient, theCache, nil 115 | } 116 | -------------------------------------------------------------------------------- /pkg/runtime/controller.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "sync" 8 | "time" 9 | 10 | "github.com/acorn-io/baaah/pkg/log" 11 | apierror "k8s.io/apimachinery/pkg/api/errors" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 14 | "k8s.io/apimachinery/pkg/runtime" 15 | "k8s.io/apimachinery/pkg/runtime/schema" 16 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 17 | "k8s.io/apimachinery/pkg/util/wait" 18 | clientgocache "k8s.io/client-go/tools/cache" 19 | "k8s.io/client-go/util/workqueue" 20 | "sigs.k8s.io/controller-runtime/pkg/cache" 21 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 22 | ) 23 | 24 | const maxTimeout2min = 2 * time.Minute 25 | 26 | type Handler interface { 27 | OnChange(key string, obj runtime.Object) error 28 | } 29 | 30 | type ResourceVersionGetter interface { 31 | GetResourceVersion() string 32 | } 33 | 34 | type HandlerFunc func(key string, obj runtime.Object) error 35 | 36 | func (h HandlerFunc) OnChange(key string, obj runtime.Object) error { 37 | return h(key, obj) 38 | } 39 | 40 | type Controller interface { 41 | Enqueue(namespace, name string) 42 | EnqueueAfter(namespace, name string, delay time.Duration) 43 | EnqueueKey(key string) 44 | Cache() (cache.Cache, error) 45 | Start(ctx context.Context, workers int) error 46 | } 47 | 48 | type controller struct { 49 | startLock sync.Mutex 50 | 51 | name string 52 | workqueue workqueue.RateLimitingInterface 53 | rateLimiter workqueue.RateLimiter 54 | informer cache.Informer 55 | handler Handler 56 | gvk schema.GroupVersionKind 57 | startKeys []startKey 58 | started bool 59 | registration clientgocache.ResourceEventHandlerRegistration 60 | obj runtime.Object 61 | cache cache.Cache 62 | } 63 | 64 | type startKey struct { 65 | key string 66 | after time.Duration 67 | } 68 | 69 | type Options struct { 70 | RateLimiter workqueue.RateLimiter 71 | } 72 | 73 | func New(gvk schema.GroupVersionKind, scheme *runtime.Scheme, cache cache.Cache, handler Handler, opts *Options) (Controller, error) { 74 | opts = applyDefaultOptions(opts) 75 | 76 | obj, err := newObject(scheme, gvk) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | informer, err := cache.GetInformerForKind(context.TODO(), gvk) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | controller := &controller{ 87 | gvk: gvk, 88 | name: gvk.String(), 89 | handler: handler, 90 | cache: cache, 91 | obj: obj, 92 | rateLimiter: opts.RateLimiter, 93 | informer: informer, 94 | } 95 | 96 | return controller, nil 97 | } 98 | 99 | func newObject(scheme *runtime.Scheme, gvk schema.GroupVersionKind) (runtime.Object, error) { 100 | obj, err := scheme.New(gvk) 101 | if runtime.IsNotRegisteredError(err) { 102 | return &unstructured.Unstructured{}, nil 103 | } 104 | return obj, err 105 | } 106 | 107 | func applyDefaultOptions(opts *Options) *Options { 108 | var newOpts Options 109 | if opts != nil { 110 | newOpts = *opts 111 | } 112 | if newOpts.RateLimiter == nil { 113 | newOpts.RateLimiter = workqueue.NewMaxOfRateLimiter( 114 | workqueue.NewItemFastSlowRateLimiter(time.Millisecond, maxTimeout2min, 30), 115 | workqueue.NewItemExponentialFailureRateLimiter(5*time.Millisecond, 30*time.Second), 116 | ) 117 | } 118 | return &newOpts 119 | } 120 | 121 | func (c *controller) Cache() (cache.Cache, error) { 122 | return c.cache, nil 123 | } 124 | 125 | func (c *controller) GroupVersionKind() schema.GroupVersionKind { 126 | return c.gvk 127 | } 128 | 129 | func (c *controller) run(ctx context.Context, workers int) { 130 | defer func() { 131 | _ = c.informer.RemoveEventHandler(c.registration) 132 | }() 133 | 134 | c.startLock.Lock() 135 | // we have to defer queue creation until we have a stopCh available because a workqueue 136 | // will create a goroutine under the hood. It we instantiate a workqueue we must have 137 | // a mechanism to Shutdown it down. Without the stopCh we don't know when to shutdown 138 | // the queue and release the goroutine 139 | c.workqueue = workqueue.NewNamedRateLimitingQueue(c.rateLimiter, c.name) 140 | for _, start := range c.startKeys { 141 | if start.after == 0 { 142 | c.workqueue.Add(start.key) 143 | } else { 144 | c.workqueue.AddAfter(start.key, start.after) 145 | } 146 | } 147 | c.startKeys = nil 148 | c.startLock.Unlock() 149 | 150 | defer utilruntime.HandleCrash() 151 | defer func() { 152 | c.workqueue.ShutDown() 153 | }() 154 | 155 | // Start the informer factories to begin populating the informer caches 156 | log.Infof("Starting %s controller", c.name) 157 | 158 | for i := 0; i < workers; i++ { 159 | go wait.Until(func() { 160 | c.runWorker(ctx) 161 | }, time.Second, ctx.Done()) 162 | } 163 | 164 | <-ctx.Done() 165 | c.startLock.Lock() 166 | defer c.startLock.Unlock() 167 | c.started = false 168 | log.Infof("Shutting down %s workers", c.name) 169 | } 170 | 171 | func (c *controller) Start(ctx context.Context, workers int) error { 172 | c.startLock.Lock() 173 | defer c.startLock.Unlock() 174 | 175 | if c.started { 176 | return nil 177 | } 178 | 179 | if c.informer == nil { 180 | informer, err := c.cache.GetInformerForKind(ctx, c.gvk) 181 | if err != nil { 182 | return err 183 | } 184 | if sii, ok := informer.(clientgocache.SharedIndexInformer); ok { 185 | c.informer = sii 186 | } else { 187 | return fmt.Errorf("expecting cache.SharedIndexInformer but got %T", informer) 188 | } 189 | } 190 | 191 | if c.registration == nil { 192 | registration, err := c.informer.AddEventHandler(clientgocache.ResourceEventHandlerFuncs{ 193 | AddFunc: c.handleObject, 194 | UpdateFunc: func(old, new interface{}) { 195 | c.handleObject(new) 196 | }, 197 | DeleteFunc: c.handleObject, 198 | }) 199 | if err != nil { 200 | return err 201 | } 202 | c.registration = registration 203 | } 204 | 205 | if !c.informer.HasSynced() { 206 | go func() { 207 | _ = c.cache.Start(ctx) 208 | }() 209 | } 210 | 211 | if ok := clientgocache.WaitForCacheSync(ctx.Done(), c.informer.HasSynced); !ok { 212 | return fmt.Errorf("failed to wait for caches to sync") 213 | } 214 | 215 | go c.run(ctx, workers) 216 | c.started = true 217 | return nil 218 | } 219 | 220 | func (c *controller) runWorker(ctx context.Context) { 221 | for c.processNextWorkItem(ctx) { 222 | } 223 | } 224 | 225 | func (c *controller) processNextWorkItem(ctx context.Context) bool { 226 | obj, shutdown := c.workqueue.Get() 227 | 228 | if shutdown { 229 | return false 230 | } 231 | 232 | if err := c.processSingleItem(ctx, obj); err != nil { 233 | if !strings.Contains(err.Error(), "please apply your changes to the latest version and try again") { 234 | log.Errorf("%v", err) 235 | } 236 | return true 237 | } 238 | 239 | return true 240 | } 241 | 242 | func (c *controller) processSingleItem(ctx context.Context, obj interface{}) error { 243 | var ( 244 | key string 245 | ok bool 246 | ) 247 | 248 | defer c.workqueue.Done(obj) 249 | 250 | if key, ok = obj.(string); !ok { 251 | c.workqueue.Forget(obj) 252 | log.Errorf("expected string in workqueue but got %#v", obj) 253 | return nil 254 | } 255 | if err := c.syncHandler(ctx, key); err != nil { 256 | c.workqueue.AddRateLimited(key) 257 | return fmt.Errorf("error syncing '%s': %s, requeuing", key, err.Error()) 258 | } 259 | 260 | c.workqueue.Forget(obj) 261 | return nil 262 | } 263 | 264 | func isSpecialKey(key string) bool { 265 | // This matches "_t " and "_r " prefixes 266 | return len(key) > 2 && key[0] == '_' && key[2] == ' ' 267 | } 268 | 269 | func (c *controller) syncHandler(ctx context.Context, key string) error { 270 | if isSpecialKey(key) { 271 | return c.handler.OnChange(key, nil) 272 | } 273 | 274 | ns, name := keyParse(key) 275 | obj := c.obj.DeepCopyObject().(kclient.Object) 276 | err := c.cache.Get(ctx, kclient.ObjectKey{ 277 | Name: name, 278 | Namespace: ns, 279 | }, obj) 280 | if apierror.IsNotFound(err) { 281 | return c.handler.OnChange(key, nil) 282 | } else if err != nil { 283 | return err 284 | } 285 | 286 | return c.handler.OnChange(key, obj.(runtime.Object)) 287 | } 288 | 289 | func (c *controller) EnqueueKey(key string) { 290 | c.startLock.Lock() 291 | defer c.startLock.Unlock() 292 | 293 | if c.workqueue == nil { 294 | c.startKeys = append(c.startKeys, startKey{key: key}) 295 | } else { 296 | c.workqueue.Add(key) 297 | } 298 | } 299 | 300 | func (c *controller) Enqueue(namespace, name string) { 301 | key := keyFunc(namespace, name) 302 | 303 | c.startLock.Lock() 304 | defer c.startLock.Unlock() 305 | 306 | if c.workqueue == nil { 307 | c.startKeys = append(c.startKeys, startKey{key: key}) 308 | } else { 309 | c.workqueue.AddRateLimited(key) 310 | } 311 | } 312 | 313 | func (c *controller) EnqueueAfter(namespace, name string, duration time.Duration) { 314 | key := keyFunc(namespace, name) 315 | 316 | c.startLock.Lock() 317 | defer c.startLock.Unlock() 318 | 319 | if c.workqueue == nil { 320 | c.startKeys = append(c.startKeys, startKey{key: key, after: duration}) 321 | } else { 322 | c.workqueue.AddAfter(key, duration) 323 | } 324 | } 325 | 326 | func keyParse(key string) (namespace string, name string) { 327 | var ok bool 328 | namespace, name, ok = strings.Cut(key, "/") 329 | if !ok { 330 | name = namespace 331 | namespace = "" 332 | } 333 | return 334 | } 335 | 336 | func keyFunc(namespace, name string) string { 337 | if namespace == "" { 338 | return name 339 | } 340 | return namespace + "/" + name 341 | } 342 | 343 | func (c *controller) enqueue(obj interface{}) { 344 | var key string 345 | var err error 346 | if key, err = clientgocache.MetaNamespaceKeyFunc(obj); err != nil { 347 | log.Errorf("%v", err) 348 | return 349 | } 350 | c.startLock.Lock() 351 | if c.workqueue == nil { 352 | c.startKeys = append(c.startKeys, startKey{key: key}) 353 | } else { 354 | c.workqueue.Add(key) 355 | } 356 | c.startLock.Unlock() 357 | } 358 | 359 | func (c *controller) handleObject(obj interface{}) { 360 | if _, ok := obj.(metav1.Object); !ok { 361 | tombstone, ok := obj.(clientgocache.DeletedFinalStateUnknown) 362 | if !ok { 363 | log.Errorf("error decoding object, invalid type") 364 | return 365 | } 366 | newObj, ok := tombstone.Obj.(metav1.Object) 367 | if !ok { 368 | log.Errorf("error decoding object tombstone, invalid type") 369 | return 370 | } 371 | obj = newObj 372 | } 373 | c.enqueue(obj) 374 | } 375 | -------------------------------------------------------------------------------- /pkg/runtime/copy.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | 8 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | ) 11 | 12 | func CopyInto(dst, src runtime.Object) error { 13 | if _, ok := src.(*unstructured.Unstructured); ok { 14 | data, err := json.Marshal(src) 15 | if err != nil { 16 | return err 17 | } 18 | return json.Unmarshal(data, dst) 19 | } 20 | src = src.DeepCopyObject() 21 | dstVal := reflect.ValueOf(dst) 22 | srcVal := reflect.ValueOf(src) 23 | if !srcVal.Type().AssignableTo(dstVal.Type()) { 24 | return fmt.Errorf("type %s not assignable to %s", srcVal.Type(), dstVal.Type()) 25 | } 26 | reflect.Indirect(dstVal).Set(reflect.Indirect(srcVal)) 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /pkg/runtime/errorcontroller.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "sigs.k8s.io/controller-runtime/pkg/cache" 8 | ) 9 | 10 | type errorController struct { 11 | err error 12 | } 13 | 14 | func newErrorController(err error) *errorController { 15 | return &errorController{err: err} 16 | } 17 | 18 | func (n *errorController) Enqueue(namespace, name string) { 19 | } 20 | 21 | func (n *errorController) EnqueueAfter(namespace, name string, delay time.Duration) { 22 | } 23 | 24 | func (n *errorController) EnqueueKey(key string) { 25 | } 26 | 27 | func (n *errorController) Cache() (cache.Cache, error) { 28 | return nil, n.err 29 | } 30 | 31 | func (n *errorController) Start(ctx context.Context, workers int) error { 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /pkg/runtime/multi/cache.go: -------------------------------------------------------------------------------- 1 | package multi 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "k8s.io/apimachinery/pkg/runtime" 8 | "k8s.io/apimachinery/pkg/runtime/schema" 9 | "sigs.k8s.io/controller-runtime/pkg/cache" 10 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 11 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 12 | ) 13 | 14 | func NewCacheNotFoundError(group string) error { 15 | return &CacheNotFoundError{group: group} 16 | } 17 | 18 | type CacheNotFoundError struct { 19 | group string 20 | } 21 | 22 | func (c *CacheNotFoundError) Error() string { 23 | return fmt.Sprintf("cache for group %s not found", c.group) 24 | } 25 | 26 | func NewCache(scheme *runtime.Scheme, defaultCache cache.Cache, caches map[string]cache.Cache) cache.Cache { 27 | return multiCache{ 28 | defaultCache: defaultCache, 29 | caches: caches, 30 | scheme: scheme, 31 | } 32 | } 33 | 34 | type multiCache struct { 35 | defaultCache cache.Cache 36 | caches map[string]cache.Cache 37 | scheme *runtime.Scheme 38 | } 39 | 40 | func (m multiCache) RemoveInformer(ctx context.Context, obj kclient.Object) error { 41 | if err := m.defaultCache.RemoveInformer(ctx, obj); err != nil { 42 | return err 43 | } 44 | for _, c := range m.caches { 45 | if err := c.RemoveInformer(ctx, obj); err != nil { 46 | return err 47 | } 48 | } 49 | return nil 50 | } 51 | 52 | func (m multiCache) Get(ctx context.Context, key kclient.ObjectKey, obj kclient.Object, opts ...kclient.GetOption) error { 53 | c, err := m.getCache(obj) 54 | if err != nil { 55 | return err 56 | } 57 | return c.Get(ctx, key, obj, opts...) 58 | } 59 | 60 | func (m multiCache) List(ctx context.Context, list kclient.ObjectList, opts ...kclient.ListOption) error { 61 | c, err := m.getCache(list) 62 | if err != nil { 63 | return err 64 | } 65 | return c.List(ctx, list, opts...) 66 | } 67 | 68 | func (m multiCache) GetInformer(ctx context.Context, obj kclient.Object, opts ...cache.InformerGetOption) (cache.Informer, error) { 69 | c, err := m.getCache(obj) 70 | if err != nil { 71 | return nil, err 72 | } 73 | return c.GetInformer(ctx, obj, opts...) 74 | } 75 | 76 | func (m multiCache) GetInformerForKind(ctx context.Context, gvk schema.GroupVersionKind, opts ...cache.InformerGetOption) (cache.Informer, error) { 77 | c, err := m.getCacheForGroup(gvk.Group) 78 | if err != nil { 79 | return nil, err 80 | } 81 | return c.GetInformerForKind(ctx, gvk, opts...) 82 | } 83 | 84 | func (m multiCache) Start(ctx context.Context) error { 85 | go func() { 86 | if err := m.defaultCache.Start(ctx); err != nil { 87 | panic(err) 88 | } 89 | }() 90 | 91 | for _, c := range m.caches { 92 | go func(c cache.Cache) { 93 | if err := c.Start(ctx); err != nil { 94 | panic(err) 95 | } 96 | }(c) 97 | } 98 | 99 | return nil 100 | } 101 | 102 | func (m multiCache) WaitForCacheSync(ctx context.Context) bool { 103 | if !m.defaultCache.WaitForCacheSync(ctx) { 104 | return false 105 | } 106 | 107 | for _, c := range m.caches { 108 | if !c.WaitForCacheSync(ctx) { 109 | return false 110 | } 111 | } 112 | return true 113 | } 114 | 115 | func (m multiCache) IndexField(ctx context.Context, obj kclient.Object, field string, extractValue kclient.IndexerFunc) error { 116 | c, err := m.getCache(obj) 117 | if err != nil { 118 | return err 119 | } 120 | return c.IndexField(ctx, obj, field, extractValue) 121 | } 122 | 123 | func (m multiCache) getCache(obj runtime.Object) (cache.Cache, error) { 124 | gvk, err := apiutil.GVKForObject(obj, m.scheme) 125 | if err != nil { 126 | return nil, err 127 | } 128 | return m.getCacheForGroup(gvk.Group) 129 | } 130 | 131 | func (m multiCache) getCacheForGroup(group string) (cache.Cache, error) { 132 | if c, ok := m.caches[group]; ok { 133 | return c, nil 134 | } else if m.defaultCache != nil { 135 | return m.defaultCache, nil 136 | } 137 | 138 | return nil, NewCacheNotFoundError(group) 139 | } 140 | -------------------------------------------------------------------------------- /pkg/runtime/multi/client.go: -------------------------------------------------------------------------------- 1 | package multi 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/acorn-io/baaah/pkg/uncached" 8 | "k8s.io/apimachinery/pkg/api/meta" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | "k8s.io/apimachinery/pkg/watch" 12 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 13 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 14 | ) 15 | 16 | func NewClientNotFoundError(group string) error { 17 | return &ClientNotFoundError{group: group} 18 | } 19 | 20 | type ClientNotFoundError struct { 21 | group string 22 | } 23 | 24 | func (c *ClientNotFoundError) Error() string { 25 | return fmt.Sprintf("client for group %s not found", c.group) 26 | } 27 | 28 | type multiClient struct { 29 | defaultClient kclient.WithWatch 30 | clients map[string]kclient.WithWatch 31 | } 32 | 33 | type clientWithFakeWatch struct { 34 | kclient.Client 35 | } 36 | 37 | func (c *clientWithFakeWatch) Watch(context.Context, kclient.ObjectList, ...kclient.ListOption) (watch.Interface, error) { 38 | return watch.NewFake(), nil 39 | } 40 | 41 | // NewClient returns a client that will use the client for the API groups it knows about. 42 | // The default client is used for any unspecified API groups. If a client cannot be found, and a default doesn't exist, 43 | // then every method will return a ClientNotFound error. 44 | // Note that defaultClient's scheme should be valid for all clients. 45 | func NewClient(defaultClient kclient.Client, clients map[string]kclient.Client) kclient.Client { 46 | fakeWatchClients := make(map[string]kclient.WithWatch, len(clients)) 47 | 48 | for group, c := range clients { 49 | fakeWatchClients[group] = &clientWithFakeWatch{c} 50 | } 51 | return NewWithWatch(&clientWithFakeWatch{defaultClient}, fakeWatchClients) 52 | } 53 | 54 | // NewWithWatch returns a client WithWatch that will use the client for the API groups it knows about. 55 | // The default client is used for any unspecified API groups. If a client cannot be found, and a default doesn't exist, 56 | // then every method will return a ClientNotFound error. 57 | // Note that defaultClient's scheme should be valid for all clients. 58 | func NewWithWatch(defaultClient kclient.WithWatch, clients map[string]kclient.WithWatch) kclient.WithWatch { 59 | return &multiClient{ 60 | defaultClient: defaultClient, 61 | clients: clients, 62 | } 63 | } 64 | 65 | func (m multiClient) Get(ctx context.Context, key kclient.ObjectKey, obj kclient.Object, opts ...kclient.GetOption) error { 66 | c, err := m.getClient(obj) 67 | if err != nil { 68 | return err 69 | } 70 | return c.Get(ctx, key, obj, opts...) 71 | } 72 | 73 | func (m multiClient) List(ctx context.Context, list kclient.ObjectList, opts ...kclient.ListOption) error { 74 | c, err := m.getClient(list) 75 | if err != nil { 76 | return err 77 | } 78 | return c.List(ctx, list, opts...) 79 | } 80 | 81 | func (m multiClient) Create(ctx context.Context, obj kclient.Object, opts ...kclient.CreateOption) error { 82 | c, err := m.getClient(obj) 83 | if err != nil { 84 | return err 85 | } 86 | return c.Create(ctx, obj, opts...) 87 | } 88 | 89 | func (m multiClient) Delete(ctx context.Context, obj kclient.Object, opts ...kclient.DeleteOption) error { 90 | c, err := m.getClient(obj) 91 | if err != nil { 92 | return err 93 | } 94 | return c.Delete(ctx, obj, opts...) 95 | } 96 | 97 | func (m multiClient) Update(ctx context.Context, obj kclient.Object, opts ...kclient.UpdateOption) error { 98 | c, err := m.getClient(obj) 99 | if err != nil { 100 | return err 101 | } 102 | return c.Update(ctx, obj, opts...) 103 | } 104 | 105 | func (m multiClient) Patch(ctx context.Context, obj kclient.Object, patch kclient.Patch, opts ...kclient.PatchOption) error { 106 | c, err := m.getClient(obj) 107 | if err != nil { 108 | return err 109 | } 110 | return c.Patch(ctx, obj, patch, opts...) 111 | } 112 | 113 | func (m multiClient) DeleteAllOf(ctx context.Context, obj kclient.Object, opts ...kclient.DeleteAllOfOption) error { 114 | c, err := m.getClient(obj) 115 | if err != nil { 116 | return err 117 | } 118 | return c.DeleteAllOf(ctx, obj, opts...) 119 | } 120 | 121 | func (m multiClient) Status() kclient.SubResourceWriter { 122 | return statusMultiClient{m} 123 | } 124 | 125 | func (m multiClient) SubResource(subResource string) kclient.SubResourceClient { 126 | return subResourceMultiClient{m, subResource} 127 | } 128 | 129 | func (m multiClient) Scheme() *runtime.Scheme { 130 | return m.defaultClient.Scheme() 131 | } 132 | 133 | func (m multiClient) RESTMapper() meta.RESTMapper { 134 | return multiRestMapper{m} 135 | } 136 | 137 | func (m multiClient) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) { 138 | return apiutil.GVKForObject(obj, m.defaultClient.Scheme()) 139 | } 140 | 141 | func (m multiClient) IsObjectNamespaced(obj runtime.Object) (bool, error) { 142 | c, err := m.getClient(obj) 143 | if err != nil { 144 | return false, err 145 | } 146 | return c.IsObjectNamespaced(obj) 147 | } 148 | 149 | func (m multiClient) Watch(ctx context.Context, obj kclient.ObjectList, opts ...kclient.ListOption) (watch.Interface, error) { 150 | c, err := m.getClient(obj) 151 | if err != nil { 152 | return nil, err 153 | } 154 | return c.Watch(ctx, obj, opts...) 155 | } 156 | 157 | func (m multiClient) getClient(obj runtime.Object) (kclient.WithWatch, error) { 158 | gvk, err := m.GroupVersionKindFor(uncached.Unwrap(obj)) 159 | if err != nil { 160 | return nil, err 161 | } 162 | return m.getClientForGroup(gvk.Group) 163 | } 164 | 165 | func (m multiClient) getClientForGroup(group string) (kclient.WithWatch, error) { 166 | if c, ok := m.clients[group]; ok { 167 | return c, nil 168 | } else if m.defaultClient != nil { 169 | return m.defaultClient, nil 170 | } 171 | 172 | return nil, NewClientNotFoundError(group) 173 | } 174 | -------------------------------------------------------------------------------- /pkg/runtime/multi/restmapper.go: -------------------------------------------------------------------------------- 1 | package multi 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/api/meta" 5 | "k8s.io/apimachinery/pkg/runtime/schema" 6 | ) 7 | 8 | type multiRestMapper struct { 9 | multiClient 10 | } 11 | 12 | func (m multiRestMapper) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) { 13 | c, err := m.getClientForGroup(resource.Group) 14 | if err != nil { 15 | return schema.GroupVersionKind{}, err 16 | } 17 | return c.RESTMapper().KindFor(resource) 18 | } 19 | 20 | func (m multiRestMapper) KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error) { 21 | c, err := m.getClientForGroup(resource.Group) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return c.RESTMapper().KindsFor(resource) 26 | } 27 | 28 | func (m multiRestMapper) ResourceFor(input schema.GroupVersionResource) (schema.GroupVersionResource, error) { 29 | c, err := m.getClientForGroup(input.Group) 30 | if err != nil { 31 | return schema.GroupVersionResource{}, err 32 | } 33 | return c.RESTMapper().ResourceFor(input) 34 | } 35 | 36 | func (m multiRestMapper) ResourcesFor(input schema.GroupVersionResource) ([]schema.GroupVersionResource, error) { 37 | c, err := m.getClientForGroup(input.Group) 38 | if err != nil { 39 | return nil, err 40 | } 41 | return c.RESTMapper().ResourcesFor(input) 42 | } 43 | 44 | func (m multiRestMapper) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) { 45 | c, err := m.getClientForGroup(gk.Group) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return c.RESTMapper().RESTMapping(gk, versions...) 50 | } 51 | 52 | func (m multiRestMapper) RESTMappings(gk schema.GroupKind, versions ...string) ([]*meta.RESTMapping, error) { 53 | c, err := m.getClientForGroup(gk.Group) 54 | if err != nil { 55 | return nil, err 56 | } 57 | return c.RESTMapper().RESTMappings(gk, versions...) 58 | } 59 | 60 | func (m multiRestMapper) ResourceSingularizer(resource string) (string, error) { 61 | for _, c := range m.clients { 62 | if res, err := c.RESTMapper().ResourceSingularizer(resource); err == nil { 63 | return res, nil 64 | } 65 | } 66 | 67 | return m.defaultClient.RESTMapper().ResourceSingularizer(resource) 68 | } 69 | -------------------------------------------------------------------------------- /pkg/runtime/multi/status.go: -------------------------------------------------------------------------------- 1 | package multi 2 | 3 | import ( 4 | "context" 5 | 6 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 7 | ) 8 | 9 | type statusMultiClient struct { 10 | multiClient 11 | } 12 | 13 | func (s statusMultiClient) Create(ctx context.Context, obj, subResource kclient.Object, opts ...kclient.SubResourceCreateOption) error { 14 | c, err := s.getClient(obj) 15 | if err != nil { 16 | return err 17 | } 18 | return c.Status().Create(ctx, obj, subResource, opts...) 19 | } 20 | 21 | func (s statusMultiClient) Update(ctx context.Context, obj kclient.Object, opts ...kclient.SubResourceUpdateOption) error { 22 | c, err := s.getClient(obj) 23 | if err != nil { 24 | return err 25 | } 26 | return c.Status().Update(ctx, obj, opts...) 27 | } 28 | 29 | func (s statusMultiClient) Patch(ctx context.Context, obj kclient.Object, patch kclient.Patch, opts ...kclient.SubResourcePatchOption) error { 30 | c, err := s.getClient(obj) 31 | if err != nil { 32 | return err 33 | } 34 | return c.Status().Patch(ctx, obj, patch, opts...) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/runtime/multi/subresource.go: -------------------------------------------------------------------------------- 1 | package multi 2 | 3 | import ( 4 | "context" 5 | 6 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 7 | ) 8 | 9 | type subResourceMultiClient struct { 10 | multiClient 11 | subResource string 12 | } 13 | 14 | func (s subResourceMultiClient) Get(ctx context.Context, obj kclient.Object, subResource kclient.Object, opts ...kclient.SubResourceGetOption) error { 15 | c, err := s.getClient(obj) 16 | if err != nil { 17 | return err 18 | } 19 | return c.SubResource(s.subResource).Get(ctx, obj, subResource, opts...) 20 | } 21 | 22 | func (s subResourceMultiClient) Create(ctx context.Context, obj kclient.Object, subResource kclient.Object, opts ...kclient.SubResourceCreateOption) error { 23 | c, err := s.getClient(obj) 24 | if err != nil { 25 | return err 26 | } 27 | return c.SubResource(s.subResource).Create(ctx, obj, subResource, opts...) 28 | } 29 | 30 | func (s subResourceMultiClient) Update(ctx context.Context, obj kclient.Object, opts ...kclient.SubResourceUpdateOption) error { 31 | c, err := s.getClient(obj) 32 | if err != nil { 33 | return err 34 | } 35 | return c.SubResource(s.subResource).Update(ctx, obj, opts...) 36 | } 37 | 38 | func (s subResourceMultiClient) Patch(ctx context.Context, obj kclient.Object, patch kclient.Patch, opts ...kclient.SubResourcePatchOption) error { 39 | c, err := s.getClient(obj) 40 | if err != nil { 41 | return err 42 | } 43 | return c.SubResource(s.subResource).Patch(ctx, obj, patch, opts...) 44 | } 45 | -------------------------------------------------------------------------------- /pkg/runtime/sharedcontroller.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "time" 7 | 8 | "k8s.io/apimachinery/pkg/api/meta" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | "sigs.k8s.io/controller-runtime/pkg/cache" 12 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 13 | ) 14 | 15 | type SharedControllerHandler interface { 16 | OnChange(key string, obj runtime.Object) (runtime.Object, error) 17 | } 18 | 19 | type SharedController interface { 20 | Controller 21 | 22 | RegisterHandler(ctx context.Context, name string, handler SharedControllerHandler) error 23 | } 24 | 25 | type SharedControllerHandlerFunc func(key string, obj runtime.Object) (runtime.Object, error) 26 | 27 | func (s SharedControllerHandlerFunc) OnChange(key string, obj runtime.Object) (runtime.Object, error) { 28 | return s(key, obj) 29 | } 30 | 31 | type sharedController struct { 32 | // this allows one to create a sharedcontroller but it will not actually be started 33 | // unless some aspect of the controllers informer is accessed or needed to be used 34 | deferredController func() (Controller, error) 35 | controller Controller 36 | handler *SharedHandler 37 | startLock sync.Mutex 38 | started bool 39 | startError error 40 | client kclient.Client 41 | gvk schema.GroupVersionKind 42 | } 43 | 44 | func (s *sharedController) Cache() (cache.Cache, error) { 45 | return s.initController().Cache() 46 | } 47 | 48 | func (s *sharedController) Enqueue(namespace, name string) { 49 | s.initController().Enqueue(namespace, name) 50 | } 51 | 52 | func (s *sharedController) EnqueueAfter(namespace, name string, delay time.Duration) { 53 | s.initController().EnqueueAfter(namespace, name, delay) 54 | } 55 | 56 | func (s *sharedController) EnqueueKey(key string) { 57 | s.initController().EnqueueKey(key) 58 | } 59 | 60 | func (s *sharedController) initController() Controller { 61 | s.startLock.Lock() 62 | defer s.startLock.Unlock() 63 | 64 | if s.controller != nil { 65 | return s.controller 66 | } 67 | 68 | controller, err := s.deferredController() 69 | if err != nil { 70 | controller = newErrorController(err) 71 | } 72 | 73 | s.startError = err 74 | s.controller = controller 75 | return s.controller 76 | } 77 | 78 | func (s *sharedController) Start(ctx context.Context, workers int) error { 79 | s.startLock.Lock() 80 | defer s.startLock.Unlock() 81 | 82 | if s.startError != nil || s.controller == nil { 83 | return s.startError 84 | } 85 | 86 | if s.started { 87 | return nil 88 | } 89 | 90 | if err := s.controller.Start(ctx, workers); err != nil { 91 | return err 92 | } 93 | s.started = true 94 | 95 | context.AfterFunc(ctx, func() { 96 | s.startLock.Lock() 97 | defer s.startLock.Unlock() 98 | s.started = false 99 | }) 100 | 101 | return nil 102 | } 103 | 104 | func (s *sharedController) RegisterHandler(ctx context.Context, name string, handler SharedControllerHandler) (returnErr error) { 105 | // Ensure that controller is initialized 106 | c := s.initController() 107 | 108 | getHandlerTransaction(ctx).do(func() { 109 | ctx, cancel := context.WithCancel(ctx) 110 | s.handler.Register(ctx, name, handler) 111 | 112 | defer func() { 113 | if returnErr == nil { 114 | context.AfterFunc(ctx, cancel) 115 | } else { 116 | cancel() 117 | } 118 | }() 119 | 120 | s.startLock.Lock() 121 | defer s.startLock.Unlock() 122 | if s.started { 123 | var ( 124 | objList runtime.Object 125 | cache cache.Cache 126 | ) 127 | 128 | objList, returnErr = s.client.Scheme().New(schema.GroupVersionKind{ 129 | Group: s.gvk.Group, 130 | Version: s.gvk.Version, 131 | Kind: s.gvk.Kind + "List", 132 | }) 133 | cache, returnErr = s.controller.Cache() 134 | if returnErr != nil { 135 | return 136 | } 137 | if returnErr = cache.List(context.TODO(), objList.(kclient.ObjectList)); returnErr != nil { 138 | return 139 | } 140 | returnErr = meta.EachListItem(objList, func(obj runtime.Object) error { 141 | mObj := obj.(kclient.Object) 142 | c.EnqueueKey(keyFunc(mObj.GetNamespace(), mObj.GetName())) 143 | return nil 144 | }) 145 | } 146 | }) 147 | 148 | return nil 149 | } 150 | -------------------------------------------------------------------------------- /pkg/runtime/sharedcontrollerfactory.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | "k8s.io/client-go/util/workqueue" 9 | "sigs.k8s.io/controller-runtime/pkg/cache" 10 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 11 | ) 12 | 13 | type SharedControllerFactory interface { 14 | ForKind(gvk schema.GroupVersionKind) (SharedController, error) 15 | Start(ctx context.Context, workers int) error 16 | } 17 | 18 | type SharedControllerFactoryOptions struct { 19 | DefaultRateLimiter workqueue.RateLimiter 20 | DefaultWorkers int 21 | 22 | KindRateLimiter map[schema.GroupVersionKind]workqueue.RateLimiter 23 | KindWorkers map[schema.GroupVersionKind]int 24 | } 25 | 26 | type sharedControllerFactory struct { 27 | controllerLock sync.RWMutex 28 | cacheStartLock sync.Mutex 29 | 30 | cache cache.Cache 31 | cacheStarted bool 32 | client kclient.Client 33 | controllers map[schema.GroupVersionKind]*sharedController 34 | 35 | rateLimiter workqueue.RateLimiter 36 | workers int 37 | kindRateLimiter map[schema.GroupVersionKind]workqueue.RateLimiter 38 | kindWorkers map[schema.GroupVersionKind]int 39 | } 40 | 41 | func NewSharedControllerFactory(c kclient.Client, cache cache.Cache, opts *SharedControllerFactoryOptions) SharedControllerFactory { 42 | opts = applyDefaultSharedOptions(opts) 43 | return &sharedControllerFactory{ 44 | cache: cache, 45 | client: c, 46 | controllers: map[schema.GroupVersionKind]*sharedController{}, 47 | workers: opts.DefaultWorkers, 48 | kindWorkers: opts.KindWorkers, 49 | rateLimiter: opts.DefaultRateLimiter, 50 | kindRateLimiter: opts.KindRateLimiter, 51 | } 52 | } 53 | 54 | func applyDefaultSharedOptions(opts *SharedControllerFactoryOptions) *SharedControllerFactoryOptions { 55 | var newOpts SharedControllerFactoryOptions 56 | if opts != nil { 57 | newOpts = *opts 58 | } 59 | if newOpts.DefaultWorkers == 0 { 60 | newOpts.DefaultWorkers = 5 61 | } 62 | return &newOpts 63 | } 64 | 65 | func (s *sharedControllerFactory) Start(ctx context.Context, defaultWorkers int) error { 66 | s.controllerLock.Lock() 67 | defer s.controllerLock.Unlock() 68 | 69 | go func() { 70 | s.cacheStartLock.Lock() 71 | defer s.cacheStartLock.Unlock() 72 | if s.cacheStarted { 73 | return 74 | } 75 | if err := s.cache.Start(ctx); err != nil { 76 | panic(err) 77 | } 78 | s.cacheStarted = true 79 | }() 80 | 81 | // copy so we can release the lock during cache wait 82 | controllersCopy := map[schema.GroupVersionKind]*sharedController{} 83 | for k, v := range s.controllers { 84 | controllersCopy[k] = v 85 | } 86 | 87 | // Do not hold lock while waiting because this can cause a deadlock if 88 | // one of the handlers you are waiting on tries to acquire this lock (by looking up 89 | // shared controller) 90 | s.controllerLock.Unlock() 91 | s.cache.WaitForCacheSync(ctx) 92 | s.controllerLock.Lock() 93 | 94 | for gvk, controller := range controllersCopy { 95 | w, err := s.getWorkers(gvk, defaultWorkers) 96 | if err != nil { 97 | return err 98 | } 99 | if err := controller.Start(ctx, w); err != nil { 100 | return err 101 | } 102 | } 103 | 104 | return nil 105 | } 106 | 107 | func (s *sharedControllerFactory) ForKind(gvk schema.GroupVersionKind) (SharedController, error) { 108 | controllerResult := s.byGVK(gvk) 109 | if controllerResult != nil { 110 | return controllerResult, nil 111 | } 112 | 113 | s.controllerLock.Lock() 114 | defer s.controllerLock.Unlock() 115 | 116 | controllerResult = s.controllers[gvk] 117 | if controllerResult != nil { 118 | return controllerResult, nil 119 | } 120 | 121 | handler := &SharedHandler{} 122 | 123 | controllerResult = &sharedController{ 124 | deferredController: func() (Controller, error) { 125 | rateLimiter, ok := s.kindRateLimiter[gvk] 126 | if !ok { 127 | rateLimiter = s.rateLimiter 128 | } 129 | 130 | return New(gvk, s.client.Scheme(), s.cache, handler, &Options{ 131 | RateLimiter: rateLimiter, 132 | }) 133 | }, 134 | handler: handler, 135 | client: s.client, 136 | gvk: gvk, 137 | } 138 | 139 | s.controllers[gvk] = controllerResult 140 | return controllerResult, nil 141 | } 142 | 143 | func (s *sharedControllerFactory) getWorkers(gvk schema.GroupVersionKind, workers int) (int, error) { 144 | w, ok := s.kindWorkers[gvk] 145 | if ok { 146 | return w, nil 147 | } 148 | if workers > 0 { 149 | return workers, nil 150 | } 151 | return s.workers, nil 152 | } 153 | 154 | func (s *sharedControllerFactory) byGVK(gvk schema.GroupVersionKind) *sharedController { 155 | s.controllerLock.RLock() 156 | defer s.controllerLock.RUnlock() 157 | return s.controllers[gvk] 158 | } 159 | -------------------------------------------------------------------------------- /pkg/runtime/sharedhandler.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "reflect" 8 | "strings" 9 | "sync" 10 | "sync/atomic" 11 | 12 | "k8s.io/apimachinery/pkg/api/meta" 13 | "k8s.io/apimachinery/pkg/runtime" 14 | ) 15 | 16 | var ( 17 | ErrIgnore = errors.New("ignore handler error") 18 | ) 19 | 20 | type handlerEntry struct { 21 | id int64 22 | name string 23 | handler SharedControllerHandler 24 | } 25 | 26 | type SharedHandler struct { 27 | // keep first because arm32 needs atomic.AddInt64 target to be mem aligned 28 | idCounter int64 29 | 30 | lock sync.RWMutex 31 | handlers []handlerEntry 32 | } 33 | 34 | func (h *SharedHandler) Register(ctx context.Context, name string, handler SharedControllerHandler) { 35 | h.lock.Lock() 36 | defer h.lock.Unlock() 37 | 38 | id := atomic.AddInt64(&h.idCounter, 1) 39 | h.handlers = append(h.handlers, handlerEntry{ 40 | id: id, 41 | name: name, 42 | handler: handler, 43 | }) 44 | 45 | context.AfterFunc(ctx, func() { 46 | h.lock.Lock() 47 | defer h.lock.Unlock() 48 | 49 | for i := range h.handlers { 50 | if h.handlers[i].id == id { 51 | h.handlers = append(h.handlers[:i], h.handlers[i+1:]...) 52 | break 53 | } 54 | } 55 | }) 56 | } 57 | 58 | func (h *SharedHandler) OnChange(key string, obj runtime.Object) error { 59 | var ( 60 | errs errorList 61 | ) 62 | h.lock.RLock() 63 | handlers := h.handlers 64 | h.lock.RUnlock() 65 | 66 | for _, handler := range handlers { 67 | newObj, err := handler.handler.OnChange(key, obj) 68 | if err != nil && !errors.Is(err, ErrIgnore) { 69 | errs = append(errs, &handlerError{ 70 | HandlerName: handler.name, 71 | Err: err, 72 | }) 73 | } 74 | 75 | if newObj != nil && !reflect.ValueOf(newObj).IsNil() { 76 | meta, err := meta.Accessor(newObj) 77 | if err == nil && meta.GetUID() != "" { 78 | // avoid using an empty object 79 | obj = newObj 80 | } else if err != nil { 81 | // assign if we can't determine metadata 82 | obj = newObj 83 | } 84 | } 85 | } 86 | 87 | return errs.ToErr() 88 | } 89 | 90 | type errorList []error 91 | 92 | func (e errorList) Error() string { 93 | buf := strings.Builder{} 94 | for _, err := range e { 95 | if buf.Len() > 0 { 96 | buf.WriteString(", ") 97 | } 98 | buf.WriteString(err.Error()) 99 | } 100 | return buf.String() 101 | } 102 | 103 | func (e errorList) ToErr() error { 104 | switch len(e) { 105 | case 0: 106 | return nil 107 | case 1: 108 | return e[0] 109 | default: 110 | return e 111 | } 112 | } 113 | 114 | func (e errorList) Cause() error { 115 | if len(e) > 0 { 116 | return e[0] 117 | } 118 | return nil 119 | } 120 | 121 | type handlerError struct { 122 | HandlerName string 123 | Err error 124 | } 125 | 126 | func (h handlerError) Error() string { 127 | return fmt.Sprintf("handler %s: %v", h.HandlerName, h.Err) 128 | } 129 | 130 | func (h handlerError) Cause() error { 131 | return h.Err 132 | } 133 | -------------------------------------------------------------------------------- /pkg/runtime/transaction.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | ) 7 | 8 | type hTransactionKey struct{} 9 | 10 | type HandlerTransaction struct { 11 | context.Context 12 | 13 | lock sync.Mutex 14 | parent context.Context 15 | todo []func() 16 | } 17 | 18 | func (h *HandlerTransaction) do(f func()) { 19 | if h == nil { 20 | f() 21 | return 22 | } 23 | 24 | h.lock.Lock() 25 | defer h.lock.Unlock() 26 | 27 | h.todo = append(h.todo, f) 28 | } 29 | 30 | func (h *HandlerTransaction) Commit() { 31 | h.lock.Lock() 32 | fs := h.todo 33 | h.todo = nil 34 | h.lock.Unlock() 35 | 36 | for _, f := range fs { 37 | f() 38 | } 39 | } 40 | 41 | func (h *HandlerTransaction) Rollback() { 42 | h.lock.Lock() 43 | h.todo = nil 44 | h.lock.Unlock() 45 | } 46 | 47 | func NewHandlerTransaction(ctx context.Context) *HandlerTransaction { 48 | ht := &HandlerTransaction{ 49 | parent: ctx, 50 | } 51 | ctx = context.WithValue(ctx, hTransactionKey{}, ht) 52 | ht.Context = ctx 53 | return ht 54 | } 55 | 56 | func getHandlerTransaction(ctx context.Context) *HandlerTransaction { 57 | v, _ := ctx.Value(hTransactionKey{}).(*HandlerTransaction) 58 | return v 59 | } 60 | -------------------------------------------------------------------------------- /pkg/typed/chan.go: -------------------------------------------------------------------------------- 1 | package typed 2 | 3 | import "time" 4 | 5 | func Debounce[T any](in <-chan T) <-chan T { 6 | result := make(chan T, 1) 7 | go func() { 8 | for msg := range in { 9 | select { 10 | case result <- msg: 11 | default: 12 | } 13 | } 14 | }() 15 | return result 16 | } 17 | 18 | func Every[T any](duration time.Duration, in <-chan T) <-chan T { 19 | result := make(chan T) 20 | go func() { 21 | var ( 22 | lastUpdate T 23 | timer = time.NewTicker(duration) 24 | ) 25 | defer close(result) 26 | defer timer.Stop() 27 | for { 28 | select { 29 | case currentUpdate, ok := <-in: 30 | if !ok { 31 | result <- lastUpdate 32 | return 33 | } 34 | lastUpdate = currentUpdate 35 | case <-timer.C: 36 | result <- lastUpdate 37 | } 38 | } 39 | }() 40 | return result 41 | } 42 | 43 | func Tee[T any](in <-chan T) (<-chan T, <-chan T) { 44 | one := make(chan T, 1) 45 | two := make(chan T, 1) 46 | 47 | go func() { 48 | defer close(one) 49 | defer close(two) 50 | for x := range in { 51 | one <- x 52 | two <- x 53 | } 54 | }() 55 | 56 | return one, two 57 | } 58 | -------------------------------------------------------------------------------- /pkg/typed/funcs.go: -------------------------------------------------------------------------------- 1 | package typed 2 | 3 | import "reflect" 4 | 5 | func New[T any]() T { 6 | var empty T 7 | t := reflect.TypeOf(empty) 8 | return reflect.New(t.Elem()).Interface().(T) 9 | } 10 | 11 | func NewAs[T any, R any]() R { 12 | var empty T 13 | t := reflect.TypeOf(empty) 14 | return reflect.New(t.Elem()).Interface().(R) 15 | } 16 | 17 | func Pointer[T any](v T) *T { 18 | return &v 19 | } 20 | -------------------------------------------------------------------------------- /pkg/typed/map.go: -------------------------------------------------------------------------------- 1 | package typed 2 | 3 | import ( 4 | "sort" 5 | 6 | "golang.org/x/exp/constraints" 7 | ) 8 | 9 | func Concat[K constraints.Ordered, V any](maps ...map[K]V) map[K]V { 10 | result := map[K]V{} 11 | for _, m := range maps { 12 | for k, v := range m { 13 | result[k] = v 14 | } 15 | } 16 | return result 17 | } 18 | 19 | func SortedValuesByKey[K constraints.Ordered, T any](data map[K]T) (result []T) { 20 | for _, k := range SortedKeys(data) { 21 | result = append(result, data[k]) 22 | } 23 | return 24 | } 25 | 26 | func SortedKeys[K constraints.Ordered, T any](data map[K]T) (result []K) { 27 | for k := range data { 28 | result = append(result, k) 29 | } 30 | sort.Slice(result, func(i, j int) bool { 31 | return result[i] < result[j] 32 | }) 33 | return 34 | } 35 | 36 | type Entry[K, V any] struct { 37 | Key K 38 | Value V 39 | } 40 | 41 | func SortedValues[K constraints.Ordered, V any](data map[K]V) (result []V) { 42 | for _, entry := range Sorted(data) { 43 | result = append(result, entry.Value) 44 | } 45 | return 46 | } 47 | 48 | func Sorted[K constraints.Ordered, V any](data map[K]V) []Entry[K, V] { 49 | var result []Entry[K, V] 50 | for _, key := range SortedKeys(data) { 51 | result = append(result, Entry[K, V]{ 52 | Key: key, 53 | Value: data[key], 54 | }) 55 | } 56 | return result 57 | } 58 | -------------------------------------------------------------------------------- /pkg/typed/slice.go: -------------------------------------------------------------------------------- 1 | package typed 2 | 3 | func MapSlice[T any, K any](values []T, mapper func(T) K) []K { 4 | result := make([]K, 0, len(values)) 5 | for _, value := range values { 6 | result = append(result, mapper(value)) 7 | } 8 | return result 9 | } 10 | -------------------------------------------------------------------------------- /pkg/uncached/uncached.go: -------------------------------------------------------------------------------- 1 | package uncached 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/runtime" 5 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 6 | ) 7 | 8 | func List(obj kclient.ObjectList) kclient.ObjectList { 9 | return &HolderList{ 10 | ObjectList: obj, 11 | } 12 | } 13 | 14 | func Get(obj kclient.Object) kclient.Object { 15 | return &Holder{ 16 | Object: obj, 17 | } 18 | } 19 | 20 | func IsWrapped(obj runtime.Object) bool { 21 | if _, ok := obj.(*Holder); ok { 22 | return true 23 | } 24 | if _, ok := obj.(*HolderList); ok { 25 | return true 26 | } 27 | return false 28 | } 29 | 30 | func Unwrap(obj runtime.Object) runtime.Object { 31 | if h, ok := obj.(*Holder); ok { 32 | return h.Object 33 | } 34 | if h, ok := obj.(*HolderList); ok { 35 | return h.ObjectList 36 | } 37 | return obj 38 | } 39 | 40 | func UnwrapList(obj kclient.ObjectList) kclient.ObjectList { 41 | if h, ok := obj.(*HolderList); ok { 42 | return h.ObjectList 43 | } 44 | return obj 45 | } 46 | 47 | type Holder struct { 48 | kclient.Object 49 | } 50 | 51 | func (h *Holder) DeepCopyObject() runtime.Object { 52 | return &Holder{Object: h.Object.DeepCopyObject().(kclient.Object)} 53 | } 54 | 55 | type HolderList struct { 56 | kclient.ObjectList 57 | } 58 | 59 | func (h *HolderList) DeepCopyObject() runtime.Object { 60 | return &HolderList{ObjectList: h.ObjectList.DeepCopyObject().(kclient.ObjectList)} 61 | } 62 | -------------------------------------------------------------------------------- /pkg/urlbuilder/urlbuilder.go: -------------------------------------------------------------------------------- 1 | package urlbuilder 2 | 3 | import ( 4 | "net/url" 5 | "path" 6 | "strings" 7 | ) 8 | 9 | type PathBuilder struct { 10 | Prefix string 11 | APIGroup string 12 | APIVersion string 13 | Namespace string 14 | Name string 15 | Resource string 16 | Subresource string 17 | SubPath string 18 | } 19 | 20 | func (u PathBuilder) URL(template *url.URL) *url.URL { 21 | cp := *template 22 | cp.Path = u.Build() 23 | return &cp 24 | } 25 | 26 | func (u PathBuilder) Build() string { 27 | p := u.Prefix 28 | if u.APIGroup != "" || u.APIVersion != "" { 29 | p = path.Join(p, u.APIGroup, u.APIVersion) 30 | } 31 | if u.Namespace != "" { 32 | p = path.Join(p, "namespaces", u.Namespace) 33 | } 34 | if u.Resource != "" { 35 | p = path.Join(p, strings.ToLower(u.Resource)) 36 | } 37 | if u.Name != "" || u.Subresource != "" || u.SubPath != "" { 38 | p = path.Join(p, u.Name, u.Subresource, u.SubPath) 39 | } 40 | return p 41 | } 42 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "runtime/debug" 6 | ) 7 | 8 | type Version struct { 9 | Tag string `json:"tag,omitempty"` 10 | Commit string `json:"commit,omitempty"` 11 | Dirty bool `json:"dirty,omitempty"` 12 | } 13 | 14 | func NewVersion(tag string) Version { 15 | v := Version{ 16 | Tag: tag, 17 | } 18 | v.Commit, v.Dirty = GitCommit() 19 | return v 20 | } 21 | 22 | func (v Version) String() string { 23 | if len(v.Commit) < 12 { 24 | return v.Tag 25 | } else if v.Dirty { 26 | return fmt.Sprintf("%s-%s-dirty", v.Tag, v.Commit[:8]) 27 | } 28 | 29 | return fmt.Sprintf("%s+%s", v.Tag, v.Commit[:8]) 30 | } 31 | 32 | func GitCommit() (commit string, dirty bool) { 33 | bi, ok := debug.ReadBuildInfo() 34 | if !ok { 35 | return "", false 36 | } 37 | for _, setting := range bi.Settings { 38 | switch setting.Key { 39 | case "vcs.modified": 40 | dirty = setting.Value == "true" 41 | case "vcs.revision": 42 | commit = setting.Value 43 | } 44 | } 45 | 46 | return 47 | } 48 | -------------------------------------------------------------------------------- /pkg/watcher/watcher.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "time" 9 | 10 | "github.com/acorn-io/baaah/pkg/log" 11 | "github.com/acorn-io/baaah/pkg/typed" 12 | apierrors "k8s.io/apimachinery/pkg/api/errors" 13 | meta2 "k8s.io/apimachinery/pkg/api/meta" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/apimachinery/pkg/fields" 16 | "k8s.io/apimachinery/pkg/labels" 17 | "k8s.io/apimachinery/pkg/runtime" 18 | "k8s.io/apimachinery/pkg/watch" 19 | "sigs.k8s.io/controller-runtime/pkg/client" 20 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 21 | ) 22 | 23 | var ( 24 | WatchTimeoutSeconds int64 = 120 25 | ) 26 | 27 | type watchFunc func(revision string) (watch.Interface, error) 28 | 29 | func doWatch[T client.Object](ctx context.Context, revision string, watchFunc watchFunc, cb func(obj T) (bool, error)) (cont bool, lastRevision string, nonTerminal error, terminal error) { 30 | lastRevision = revision 31 | result, err := watchFunc(revision) 32 | if apierrors.IsGone(err) { 33 | // The server has told us the revision is not accessible anymore. There's no way to recover that 34 | // wouldn't give the caller an inconsistent view of the data (some changes could have been missed) 35 | // so we treat this as terminal 36 | return false, lastRevision, nil, err 37 | } else if err != nil { 38 | return false, lastRevision, err, nil 39 | } 40 | defer func() { 41 | result.Stop() 42 | for range result.ResultChan() { 43 | } 44 | }() 45 | 46 | for { 47 | select { 48 | case <-ctx.Done(): 49 | return false, lastRevision, nil, fmt.Errorf("terminating watch: %w", ctx.Err()) 50 | case event, open := <-result.ResultChan(): 51 | if !open { 52 | return false, lastRevision, nil, nil 53 | } 54 | switch event.Type { 55 | case watch.Bookmark: 56 | m, err := meta2.Accessor(event.Object) 57 | if err == nil && m.GetResourceVersion() != "" { 58 | lastRevision = m.GetResourceVersion() 59 | } 60 | case watch.Deleted: 61 | o := event.Object.DeepCopyObject() 62 | mo := o.(client.Object) 63 | changed := false 64 | if mo.GetDeletionTimestamp().IsZero() { 65 | now := metav1.Now() 66 | mo.SetDeletionTimestamp(&now) 67 | changed = true 68 | } 69 | if len(mo.GetFinalizers()) > 0 { 70 | mo.SetFinalizers(nil) 71 | changed = true 72 | } 73 | if changed { 74 | event.Object = mo 75 | } 76 | fallthrough 77 | case watch.Added, watch.Modified: 78 | done, err := cb(event.Object.(T)) 79 | if apierrors.IsConflict(err) { 80 | // if we got a conflict, return a false (not done) and nil for error 81 | return false, lastRevision, err, nil 82 | } 83 | if err != nil { 84 | return false, lastRevision, nil, err 85 | } 86 | if done { 87 | return true, lastRevision, nil, nil 88 | } 89 | m, err := meta2.Accessor(event.Object) 90 | if err == nil && m.GetResourceVersion() != "" { 91 | lastRevision = m.GetResourceVersion() 92 | } 93 | case watch.Error: 94 | return false, lastRevision, nil, apierrors.FromObject(event.Object) 95 | } 96 | } 97 | } 98 | } 99 | 100 | func retryWatch[T client.Object](ctx context.Context, revision string, watchFunc watchFunc, cb func(obj T) (bool, error)) (T, error) { 101 | var last T 102 | newCB := func(obj T) (bool, error) { 103 | last = obj 104 | return cb(obj) 105 | } 106 | for { 107 | o := typed.New[T]() 108 | done, lastRevision, err, terminalErr := doWatch(ctx, revision, watchFunc, newCB) 109 | if err != nil { 110 | if !errors.Is(err, context.Canceled) && !strings.Contains(err.Error(), "context canceled") { 111 | log.Errorf("error while watching type %T: %v", o, err) 112 | } 113 | } else if terminalErr != nil { 114 | if !errors.Is(terminalErr, context.DeadlineExceeded) && !errors.Is(terminalErr, context.Canceled) && !strings.Contains(terminalErr.Error(), "context canceled") { 115 | log.Errorf("terminal error while watching type %T: %v", o, terminalErr) 116 | } 117 | return last, terminalErr 118 | } else if done { 119 | return last, nil 120 | } 121 | if lastRevision != "" { 122 | revision = lastRevision 123 | } 124 | log.Debugf("no error, going to restart watch %T from revision %s", o, revision) 125 | select { 126 | case <-ctx.Done(): 127 | return last, ctx.Err() 128 | case <-time.After(2 * time.Second): 129 | } 130 | } 131 | } 132 | 133 | type Watcher[T client.Object] struct { 134 | client client.WithWatch 135 | scheme *runtime.Scheme 136 | } 137 | 138 | func New[T client.Object](client client.WithWatch) *Watcher[T] { 139 | return &Watcher[T]{ 140 | client: client, 141 | scheme: client.Scheme(), 142 | } 143 | } 144 | 145 | func (w *Watcher[T]) newListObj() (client.ObjectList, error) { 146 | obj := typed.New[T]() 147 | gvk, err := apiutil.GVKForObject(obj, w.scheme) 148 | if err != nil { 149 | return nil, err 150 | } 151 | gvk.Kind += "List" 152 | listObj, err := w.scheme.New(gvk) 153 | if err != nil { 154 | return nil, err 155 | } 156 | clientListObj, ok := listObj.(client.ObjectList) 157 | if !ok { 158 | return nil, fmt.Errorf("%T is not a client.ObjectList", listObj) 159 | } 160 | return clientListObj, nil 161 | } 162 | 163 | func (w *Watcher[T]) ByObject(ctx context.Context, obj T, cb func(obj T) (bool, error)) (def T, _ error) { 164 | return w.ByName(ctx, obj.GetNamespace(), obj.GetName(), cb) 165 | } 166 | 167 | func (w *Watcher[T]) ByName(ctx context.Context, namespace, name string, cb func(obj T) (bool, error)) (def T, _ error) { 168 | return w.bySelector(ctx, namespace, nil, fields.SelectorFromSet(map[string]string{ 169 | "metadata.name": name, 170 | }), cb) 171 | } 172 | 173 | func (w *Watcher[T]) BySelector(ctx context.Context, namespace string, selector labels.Selector, cb func(obj T) (bool, error)) (def T, _ error) { 174 | return w.bySelector(ctx, namespace, selector, nil, cb) 175 | } 176 | 177 | func (w *Watcher[T]) bySelector(ctx context.Context, namespace string, selector labels.Selector, fieldSelector fields.Selector, cb func(obj T) (bool, error)) (def T, _ error) { 178 | listObj, err := w.newListObj() 179 | if err != nil { 180 | return def, err 181 | } 182 | 183 | err = w.client.List(ctx, listObj, &client.ListOptions{ 184 | Namespace: namespace, 185 | LabelSelector: selector, 186 | FieldSelector: fieldSelector, 187 | }) 188 | if err != nil { 189 | return def, err 190 | } 191 | rev := listObj.GetResourceVersion() 192 | 193 | var ( 194 | doneObj T 195 | doneSet bool 196 | ) 197 | err = meta2.EachListItem(listObj, func(object runtime.Object) error { 198 | done, err := cb(object.(T)) 199 | if done { 200 | doneObj = object.(T) 201 | doneSet = true 202 | } 203 | return err 204 | }) 205 | if doneSet || err != nil { 206 | return doneObj, err 207 | } 208 | 209 | return retryWatch(ctx, rev, func(revision string) (watch.Interface, error) { 210 | return w.client.Watch(ctx, listObj, &client.ListOptions{ 211 | LabelSelector: selector, 212 | FieldSelector: fieldSelector, 213 | Namespace: namespace, 214 | Raw: &metav1.ListOptions{ 215 | ResourceVersion: revision, 216 | AllowWatchBookmarks: true, 217 | }, 218 | }) 219 | }, cb) 220 | } 221 | -------------------------------------------------------------------------------- /pkg/webhook/match.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | v1 "k8s.io/api/admission/v1" 5 | ) 6 | 7 | type RouteMatch struct { 8 | handler Handler 9 | kind string 10 | resource string 11 | version string 12 | subResource string 13 | dryRun *bool 14 | group string 15 | name string 16 | namespace string 17 | operation v1.Operation 18 | } 19 | 20 | func (r *RouteMatch) admit(response *Response, request *Request) error { 21 | if r.handler != nil { 22 | return r.handler.Admit(response, request) 23 | } 24 | return nil 25 | } 26 | 27 | func (r *RouteMatch) matches(req *v1.AdmissionRequest) bool { 28 | var ( 29 | group, version, kind, resource string 30 | ) 31 | if req.RequestKind != nil { 32 | group, version, kind = req.RequestKind.Group, req.RequestKind.Version, req.RequestKind.Kind 33 | } 34 | if req.RequestResource != nil { 35 | group, version, resource = req.RequestResource.Group, req.RequestResource.Version, req.RequestResource.Resource 36 | } 37 | 38 | return checkString(r.kind, kind) && 39 | checkString(r.resource, resource) && 40 | checkString(r.subResource, req.SubResource) && 41 | checkString(r.version, version) && 42 | checkString(r.group, group) && 43 | checkString(r.name, req.Name) && 44 | checkString(r.namespace, req.Namespace) && 45 | checkString(string(r.operation), string(req.Operation)) && 46 | checkBool(r.dryRun, req.DryRun) 47 | } 48 | 49 | func checkString(expected, actual string) bool { 50 | if expected == "" { 51 | return true 52 | } 53 | return expected == actual 54 | } 55 | 56 | func checkBool(expected, actual *bool) bool { 57 | if expected == nil { 58 | return true 59 | } 60 | if actual == nil { 61 | return false 62 | } 63 | return *expected == *actual 64 | } 65 | 66 | // Pretty methods 67 | 68 | func (r *RouteMatch) DryRun(dryRun bool) *RouteMatch { r.dryRun = &dryRun; return r } 69 | func (r *RouteMatch) Group(group string) *RouteMatch { r.group = group; return r } 70 | func (r *RouteMatch) HandleFunc(handler HandlerFunc) { r.handler = handler } 71 | func (r *RouteMatch) Handle(handler Handler) { r.handler = handler } 72 | func (r *RouteMatch) Kind(kind string) *RouteMatch { r.kind = kind; return r } 73 | func (r *RouteMatch) Name(name string) *RouteMatch { r.name = name; return r } 74 | func (r *RouteMatch) Namespace(namespace string) *RouteMatch { r.namespace = namespace; return r } 75 | func (r *RouteMatch) Operation(operation v1.Operation) *RouteMatch { r.operation = operation; return r } 76 | func (r *RouteMatch) Resource(resource string) *RouteMatch { r.resource = resource; return r } 77 | func (r *RouteMatch) SubResource(sr string) *RouteMatch { r.subResource = sr; return r } 78 | func (r *RouteMatch) Version(version string) *RouteMatch { r.version = version; return r } 79 | 80 | // Wrappers for pretty methods 81 | 82 | func (r *Router) DryRun(dryRun bool) *RouteMatch { return r.next().DryRun(dryRun) } 83 | func (r *Router) Group(group string) *RouteMatch { return r.next().Group(group) } 84 | func (r *Router) HandleFunc(hf HandlerFunc) { r.next().HandleFunc(hf) } 85 | func (r *Router) Handle(handler Handler) { r.next().Handle(handler) } 86 | func (r *Router) Kind(kind string) *RouteMatch { return r.next().Kind(kind) } 87 | func (r *Router) Name(name string) *RouteMatch { return r.next().Name(name) } 88 | func (r *Router) Namespace(namespace string) *RouteMatch { return r.next().Namespace(namespace) } 89 | func (r *Router) Operation(operation v1.Operation) *RouteMatch { return r.next().Operation(operation) } 90 | func (r *Router) Resource(resource string) *RouteMatch { return r.next().Resource(resource) } 91 | func (r *Router) SubResource(subResource string) *RouteMatch { 92 | return r.next().SubResource(subResource) 93 | } 94 | func (r *Router) Version(version string) *RouteMatch { return r.next().Version(version) } 95 | -------------------------------------------------------------------------------- /pkg/webhook/router.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | 9 | "github.com/acorn-io/baaah/pkg/log" 10 | "gomodules.xyz/jsonpatch/v2" 11 | v1 "k8s.io/api/admission/v1" 12 | "k8s.io/apimachinery/pkg/api/errors" 13 | kclient "sigs.k8s.io/controller-runtime/pkg/client" 14 | ) 15 | 16 | var ( 17 | jsonPatchType = v1.PatchTypeJSONPatch 18 | ) 19 | 20 | func NewRouter() *Router { 21 | return &Router{} 22 | } 23 | 24 | type Router struct { 25 | matches []*RouteMatch 26 | } 27 | 28 | func (r *Router) sendError(rw http.ResponseWriter, review *v1.AdmissionReview, err error) { 29 | log.Errorf("%v", err) 30 | if review == nil || review.Request == nil { 31 | http.Error(rw, err.Error(), http.StatusInternalServerError) 32 | return 33 | } 34 | review.Response.Allowed = false 35 | review.Response.Result = &errors.NewInternalError(err).ErrStatus 36 | writeResponse(rw, review) 37 | } 38 | 39 | func writeResponse(rw http.ResponseWriter, review *v1.AdmissionReview) { 40 | rw.Header().Set("Content-Type", "application/json") 41 | _ = json.NewEncoder(rw).Encode(review) 42 | } 43 | 44 | func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 45 | review := &v1.AdmissionReview{} 46 | err := json.NewDecoder(req.Body).Decode(review) 47 | if err != nil { 48 | r.sendError(rw, review, err) 49 | return 50 | } 51 | 52 | if review.Request == nil { 53 | r.sendError(rw, review, fmt.Errorf("request is not set")) 54 | return 55 | } 56 | 57 | response := &Response{ 58 | AdmissionResponse: v1.AdmissionResponse{ 59 | UID: review.Request.UID, 60 | }, 61 | } 62 | 63 | review.Response = &response.AdmissionResponse 64 | 65 | if err := r.admit(response, review.Request, req); err != nil { 66 | r.sendError(rw, review, err) 67 | return 68 | } 69 | 70 | writeResponse(rw, review) 71 | } 72 | 73 | func (r *Router) admit(response *Response, request *v1.AdmissionRequest, req *http.Request) error { 74 | for _, m := range r.matches { 75 | if m.matches(request) { 76 | err := m.admit(response, &Request{ 77 | AdmissionRequest: *request, 78 | Context: req.Context(), 79 | }) 80 | log.Debugf("admit result: %s %s %s user=%s allowed=%v err=%v", request.Operation, request.Kind.String(), resourceString(request.Namespace, request.Name), request.UserInfo.Username, response.Allowed, err) 81 | return err 82 | } 83 | } 84 | return fmt.Errorf("no route match found for %s %s %s", request.Operation, request.Kind.String(), resourceString(request.Namespace, request.Name)) 85 | } 86 | 87 | func (r *Router) next() *RouteMatch { 88 | match := &RouteMatch{} 89 | r.matches = append(r.matches, match) 90 | return match 91 | } 92 | 93 | type Request struct { 94 | v1.AdmissionRequest 95 | 96 | Context context.Context 97 | } 98 | 99 | func (r *Request) DecodeOldObject(obj kclient.Object) error { 100 | return json.Unmarshal(r.OldObject.Raw, obj) 101 | } 102 | 103 | func (r *Request) DecodeObject(obj kclient.Object) error { 104 | return json.Unmarshal(r.Object.Raw, obj) 105 | } 106 | 107 | type Response struct { 108 | v1.AdmissionResponse 109 | } 110 | 111 | func (r *Response) CreatePatch(request *Request, newObj kclient.Object) error { 112 | if len(r.Patch) > 0 { 113 | return fmt.Errorf("response patch has already been already been assigned") 114 | } 115 | 116 | newBytes, err := json.Marshal(newObj) 117 | if err != nil { 118 | return err 119 | } 120 | 121 | patch, err := jsonpatch.CreatePatch(request.Object.Raw, newBytes) 122 | if err != nil { 123 | return err 124 | } 125 | 126 | patchData, err := json.Marshal(patch) 127 | if err != nil { 128 | return err 129 | } 130 | 131 | r.Patch = patchData 132 | r.PatchType = &jsonPatchType 133 | return nil 134 | } 135 | 136 | type Handler interface { 137 | Admit(resp *Response, req *Request) error 138 | } 139 | 140 | type HandlerFunc func(resp *Response, req *Request) error 141 | 142 | func (h HandlerFunc) Admit(resp *Response, req *Request) error { 143 | return h(resp, req) 144 | } 145 | 146 | // resourceString returns the resource formatted as a string 147 | func resourceString(ns, name string) string { 148 | if ns == "" { 149 | return name 150 | } 151 | return fmt.Sprintf("%s/%s", ns, name) 152 | } 153 | -------------------------------------------------------------------------------- /pkg/yaml/yaml.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "strings" 10 | 11 | "github.com/acorn-io/baaah/pkg/data" 12 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 13 | "k8s.io/apimachinery/pkg/runtime" 14 | yamlDecoder "k8s.io/apimachinery/pkg/util/yaml" 15 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 16 | "sigs.k8s.io/yaml" 17 | ) 18 | 19 | var ( 20 | cleanPrefix = []string{ 21 | "kubectl.kubernetes.io/", 22 | } 23 | cleanContains = []string{ 24 | "cattle.io/", 25 | } 26 | ) 27 | 28 | func Unmarshal(data []byte, v interface{}) error { 29 | return yamlDecoder.NewYAMLToJSONDecoder(bytes.NewBuffer(data)).Decode(v) 30 | } 31 | 32 | func ToObjects(in io.Reader) ([]runtime.Object, error) { 33 | var result []runtime.Object 34 | reader := yamlDecoder.NewYAMLReader(bufio.NewReaderSize(in, 4096)) 35 | for { 36 | raw, err := reader.Read() 37 | if err == io.EOF { 38 | break 39 | } 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | obj, err := toObjects(raw) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | result = append(result, obj...) 50 | } 51 | 52 | return result, nil 53 | } 54 | 55 | func toObjects(bytes []byte) ([]runtime.Object, error) { 56 | bytes, err := yamlDecoder.ToJSON(bytes) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | check := map[string]interface{}{} 62 | if err := json.Unmarshal(bytes, &check); err != nil || len(check) == 0 { 63 | return nil, err 64 | } 65 | 66 | obj, _, err := unstructured.UnstructuredJSONScheme.Decode(bytes, nil, nil) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | if l, ok := obj.(*unstructured.UnstructuredList); ok { 72 | var result []runtime.Object 73 | for _, obj := range l.Items { 74 | copy := obj 75 | result = append(result, ©) 76 | } 77 | return result, nil 78 | } 79 | 80 | return []runtime.Object{obj}, nil 81 | } 82 | 83 | // Export will attempt to clean up the objects a bit before 84 | // rendering to yaml so that they can easily be imported into another 85 | // cluster 86 | func Export(scheme *runtime.Scheme, objects ...runtime.Object) ([]byte, error) { 87 | if len(objects) == 0 { 88 | return nil, nil 89 | } 90 | 91 | buffer := &bytes.Buffer{} 92 | for i, obj := range objects { 93 | if i > 0 { 94 | buffer.WriteString("\n---\n") 95 | } 96 | 97 | obj, err := CleanObjectForExport(scheme, obj) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | bytes, err := yaml.Marshal(obj) 103 | if err != nil { 104 | return nil, fmt.Errorf("failed to encode %s: %w", obj.GetObjectKind().GroupVersionKind(), err) 105 | } 106 | buffer.Write(bytes) 107 | } 108 | 109 | return buffer.Bytes(), nil 110 | } 111 | 112 | func CleanObjectForExport(scheme *runtime.Scheme, obj runtime.Object) (runtime.Object, error) { 113 | obj = obj.DeepCopyObject() 114 | if obj.GetObjectKind().GroupVersionKind().Kind == "" { 115 | if gvk, err := apiutil.GVKForObject(obj, scheme); err == nil { 116 | obj.GetObjectKind().SetGroupVersionKind(gvk) 117 | } else if err != nil { 118 | return nil, fmt.Errorf("kind and/or apiVersion is not set on input object %v: %w", obj, err) 119 | } 120 | } 121 | 122 | data, err := data.ToMapInterface(obj) 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | unstr := &unstructured.Unstructured{ 128 | Object: data, 129 | } 130 | 131 | metadata := map[string]interface{}{} 132 | 133 | if name := unstr.GetName(); len(name) > 0 { 134 | metadata["name"] = name 135 | } else if generated := unstr.GetGenerateName(); len(generated) > 0 { 136 | metadata["generateName"] = generated 137 | } else { 138 | return nil, fmt.Errorf("either name or generateName must be set on obj: %v", obj) 139 | } 140 | 141 | if unstr.GetNamespace() != "" { 142 | metadata["namespace"] = unstr.GetNamespace() 143 | } 144 | if annotations := unstr.GetAnnotations(); len(annotations) > 0 { 145 | cleanMap(annotations) 146 | if len(annotations) > 0 { 147 | metadata["annotations"] = annotations 148 | } else { 149 | delete(metadata, "annotations") 150 | } 151 | } 152 | if labels := unstr.GetLabels(); len(labels) > 0 { 153 | cleanMap(labels) 154 | if len(labels) > 0 { 155 | metadata["labels"] = labels 156 | } else { 157 | delete(metadata, "labels") 158 | } 159 | } 160 | 161 | if spec, ok := data["spec"]; ok { 162 | if spec == nil { 163 | delete(data, "spec") 164 | } else if m, ok := spec.(map[string]interface{}); ok && len(m) == 0 { 165 | delete(data, "spec") 166 | } 167 | } 168 | 169 | data["metadata"] = metadata 170 | delete(data, "status") 171 | 172 | return unstr, nil 173 | } 174 | 175 | func CleanAnnotationsForExport(annotations map[string]string) map[string]string { 176 | result := make(map[string]string, len(annotations)) 177 | 178 | outer: 179 | for k := range annotations { 180 | for _, prefix := range cleanPrefix { 181 | if strings.HasPrefix(k, prefix) { 182 | continue outer 183 | } 184 | } 185 | for _, contains := range cleanContains { 186 | if strings.Contains(k, contains) { 187 | continue outer 188 | } 189 | } 190 | result[k] = annotations[k] 191 | } 192 | return result 193 | } 194 | 195 | func cleanMap(annoLabels map[string]string) { 196 | for k := range annoLabels { 197 | for _, prefix := range cleanPrefix { 198 | if strings.HasPrefix(k, prefix) { 199 | delete(annoLabels, k) 200 | } 201 | } 202 | } 203 | } 204 | 205 | func ToBytes(objects []runtime.Object) ([]byte, error) { 206 | if len(objects) == 0 { 207 | return nil, nil 208 | } 209 | 210 | buffer := &bytes.Buffer{} 211 | for i, obj := range objects { 212 | if i > 0 { 213 | buffer.WriteString("\n---\n") 214 | } 215 | 216 | bytes, err := yaml.Marshal(obj) 217 | if err != nil { 218 | return nil, fmt.Errorf("failed to encode %s: %w", obj.GetObjectKind().GroupVersionKind(), err) 219 | } 220 | buffer.Write(bytes) 221 | } 222 | 223 | return buffer.Bytes(), nil 224 | } 225 | -------------------------------------------------------------------------------- /router.go: -------------------------------------------------------------------------------- 1 | package baaah 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/acorn-io/baaah/pkg/backend" 7 | "github.com/acorn-io/baaah/pkg/leader" 8 | "github.com/acorn-io/baaah/pkg/restconfig" 9 | "github.com/acorn-io/baaah/pkg/router" 10 | bruntime "github.com/acorn-io/baaah/pkg/runtime" 11 | "k8s.io/apimachinery/pkg/runtime" 12 | "k8s.io/client-go/rest" 13 | ) 14 | 15 | const defaultHealthzPort = 8888 16 | 17 | type Options struct { 18 | // If the backend is nil, then DefaultRESTConfig, DefaultNamespace, and Scheme are used to create a backend. 19 | Backend backend.Backend 20 | // If a Backend is provided, then this is ignored. If not provided and needed, then a default is created with Scheme. 21 | DefaultRESTConfig *rest.Config 22 | // If a Backend is provided, then this is ignored. 23 | DefaultNamespace string 24 | // If a Backend is provided, then this is ignored. 25 | Scheme *runtime.Scheme 26 | // APIGroupConfigs are keyed by an API group. This indicates to the router that all actions on this group should use the 27 | // given Config. This is useful for routers that watch different objects on different API servers. 28 | APIGroupConfigs map[string]bruntime.Config 29 | // ElectionConfig being nil represents no leader election for the router. 30 | ElectionConfig *leader.ElectionConfig 31 | // Defaults to 8888 32 | HealthzPort int 33 | } 34 | 35 | func (o *Options) complete() (*Options, error) { 36 | var result Options 37 | if o != nil { 38 | result = *o 39 | } 40 | 41 | if result.Scheme == nil { 42 | return nil, fmt.Errorf("scheme is required to be set") 43 | } 44 | 45 | if result.HealthzPort == 0 { 46 | result.HealthzPort = defaultHealthzPort 47 | } 48 | 49 | if result.Backend != nil { 50 | return &result, nil 51 | } 52 | 53 | if result.DefaultRESTConfig == nil { 54 | var err error 55 | result.DefaultRESTConfig, err = restconfig.New(result.Scheme) 56 | if err != nil { 57 | return nil, err 58 | } 59 | } 60 | 61 | defaultConfig := bruntime.Config{Rest: result.DefaultRESTConfig, Namespace: result.DefaultNamespace} 62 | backend, err := bruntime.NewRuntimeWithConfigs(defaultConfig, result.APIGroupConfigs, result.Scheme) 63 | if err != nil { 64 | return nil, err 65 | } 66 | result.Backend = backend.Backend 67 | 68 | return &result, nil 69 | } 70 | 71 | // DefaultOptions represent the standard options for a Router. 72 | // The default leader election uses a lease lock and a TTL of 15 seconds. 73 | func DefaultOptions(routerName string, scheme *runtime.Scheme) (*Options, error) { 74 | cfg, err := restconfig.New(scheme) 75 | if err != nil { 76 | return nil, err 77 | } 78 | rt, err := bruntime.NewRuntimeForNamespace(cfg, "", scheme) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | return &Options{ 84 | Backend: rt.Backend, 85 | DefaultRESTConfig: cfg, 86 | Scheme: scheme, 87 | ElectionConfig: leader.NewDefaultElectionConfig("", routerName, cfg), 88 | HealthzPort: defaultHealthzPort, 89 | }, nil 90 | } 91 | 92 | // DefaultRouter The routerName is important as this name will be used to assign ownership of objects created by this 93 | // router. Specifically the routerName is assigned to the sub-context in the apply actions. Additionally, the routerName 94 | // will be used for the leader election lease lock. 95 | func DefaultRouter(routerName string, scheme *runtime.Scheme) (*router.Router, error) { 96 | opts, err := DefaultOptions(routerName, scheme) 97 | if err != nil { 98 | return nil, err 99 | } 100 | return NewRouter(routerName, opts) 101 | } 102 | 103 | func NewRouter(handlerName string, opts *Options) (*router.Router, error) { 104 | opts, err := opts.complete() 105 | if err != nil { 106 | return nil, err 107 | } 108 | return router.New(router.NewHandlerSet(handlerName, opts.Backend.Scheme(), opts.Backend), opts.ElectionConfig, opts.HealthzPort), nil 109 | } 110 | --------------------------------------------------------------------------------