├── .github └── workflows │ ├── test-one.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── cli-runtime-flags ├── .main.go.swp ├── Makefile ├── README.md ├── go.mod └── main.go ├── cli-runtime-printers ├── Makefile ├── README.md ├── go.mod └── main.go ├── cli-runtime-resources-from-cluster ├── Makefile ├── README.md ├── go.mod └── main.go ├── cli-runtime-resources-from-file ├── Makefile ├── README.md ├── go.mod ├── main.go └── resources.yaml ├── convert-unstructured-typed ├── Makefile ├── README.md ├── go.mod └── main.go ├── crud-dynamic-simple ├── Makefile ├── README.md ├── go.mod └── main.go ├── crud-typed-simple ├── Makefile ├── README.md ├── go.mod └── main.go ├── error-handling ├── Makefile ├── README.md ├── go.mod └── main.go ├── field-selectors ├── Makefile ├── README.md ├── go.mod └── main.go ├── go.work ├── go.work.sum ├── informer-dynamic-simple ├── Makefile ├── README.md ├── go.mod └── main.go ├── informer-typed-simple ├── Makefile ├── README.md ├── go.mod └── main.go ├── kubeconfig-default-context ├── Makefile ├── README.md ├── go.mod └── main.go ├── kubeconfig-from-yaml ├── Makefile ├── README.md ├── go.mod └── main.go ├── kubeconfig-list-contexts ├── Makefile ├── README.md ├── go.mod └── main.go ├── kubeconfig-overridden-context ├── Makefile ├── README.md ├── go.mod └── main.go ├── label-selectors ├── Makefile ├── README.md ├── go.mod └── main.go ├── list-typed-simple ├── Makefile ├── README.md ├── go.mod └── main.go ├── patch-add-ephemeral-container ├── Makefile ├── README.md ├── go.mod └── main.go ├── retry-on-conflict ├── Makefile ├── README.md ├── go.mod └── main.go ├── serialize-typed-json ├── Makefile ├── README.md ├── go.mod └── main.go ├── serialize-typed-yaml ├── Makefile ├── README.md ├── go.mod └── main.go ├── serialize-unstructured-json ├── Makefile ├── README.md ├── go.mod └── main.go ├── serialize-unstructured-yaml ├── Makefile ├── README.md ├── go.mod └── main.go ├── watch-typed-simple ├── Makefile ├── README.md ├── go.mod └── main.go └── workqueue ├── Makefile ├── README.md ├── go.mod └── main.go /.github/workflows/test-one.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | example_program: 5 | required: true 6 | type: string 7 | k8s_cluster_ver: 8 | required: true 9 | type: string 10 | k8s_package_ver: 11 | required: true 12 | type: string 13 | 14 | jobs: 15 | test-one: 16 | timeout-minutes: 5 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 1 22 | - name: Install Go 23 | uses: actions/setup-go@v3 24 | with: 25 | go-version: "1.22.10" 26 | - name: Create test Kubernetes cluster (using kind) 27 | uses: helm/kind-action@v1.5.0 28 | with: 29 | node_image: kindest/node:v${{ inputs.k8s_cluster_ver }} 30 | - name: Run mini-program ${{ inputs.example_program }} on Kubernetes v${{ inputs.k8s_cluster_ver }} using client-go v${{ inputs.k8s_package_ver }} 31 | run: | 32 | cd ${{ inputs.example_program }} 33 | go mod edit -replace k8s.io/client-go=k8s.io/client-go@v${{ inputs.k8s_package_ver }} 34 | go mod edit -replace k8s.io/api=k8s.io/api@v${{ inputs.k8s_package_ver }} 35 | go mod edit -replace k8s.io/apimachinery=k8s.io/apimachinery@v${{ inputs.k8s_package_ver }} 36 | go mod edit -replace k8s.io/cli-runtime=k8s.io/cli-runtime@v${{ inputs.k8s_package_ver }} 37 | go mod tidy 38 | make test 39 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test example mini-programs against different versions of Kubernetes & client-go 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | types: [opened, synchronize, closed] 11 | 12 | jobs: 13 | prepare: 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 1 16 | steps: 17 | - uses: actions/checkout@v3 18 | - id: make-list 19 | name: Prepare mini-program list 20 | run: | 21 | echo "dirs=$(ls -d */ | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT 22 | outputs: 23 | dirs: ${{ steps.make-list.outputs.dirs }} 24 | 25 | client_go_0_28: 26 | needs: prepare 27 | strategy: 28 | fail-fast: false 29 | # A matrix can produce no more than 256 elements, 30 | # so testing too many k8s versions is unfeasible. 31 | matrix: 32 | example_program: ${{ fromJson(needs.prepare.outputs.dirs) }} 33 | k8s_cluster_ver: 34 | - "1.28.9" 35 | - "1.29.12" 36 | - "1.30.8" 37 | - "1.31.4" 38 | uses: ./.github/workflows/test-one.yml 39 | with: 40 | example_program: ${{ matrix.example_program }} 41 | k8s_cluster_ver: ${{ matrix.k8s_cluster_ver }} 42 | k8s_package_ver: "0.28.14" 43 | 44 | client_go_0_29: 45 | needs: prepare 46 | strategy: 47 | fail-fast: false 48 | # A matrix can produce no more than 256 elements, 49 | # so testing too many k8s versions is unfeasible. 50 | matrix: 51 | example_program: ${{ fromJson(needs.prepare.outputs.dirs) }} 52 | k8s_cluster_ver: 53 | - "1.28.9" 54 | - "1.29.12" 55 | - "1.30.8" 56 | - "1.31.4" 57 | uses: ./.github/workflows/test-one.yml 58 | with: 59 | example_program: ${{ matrix.example_program }} 60 | k8s_cluster_ver: ${{ matrix.k8s_cluster_ver }} 61 | k8s_package_ver: "0.29.12" 62 | 63 | client_go_0_30: 64 | needs: prepare 65 | strategy: 66 | fail-fast: false 67 | # A matrix can produce no more than 256 elements, 68 | # so testing too many k8s versions is unfeasible. 69 | matrix: 70 | example_program: ${{ fromJson(needs.prepare.outputs.dirs) }} 71 | k8s_cluster_ver: 72 | - "1.28.9" 73 | - "1.29.12" 74 | - "1.30.8" 75 | - "1.31.4" 76 | uses: ./.github/workflows/test-one.yml 77 | with: 78 | example_program: ${{ matrix.example_program }} 79 | k8s_cluster_ver: ${{ matrix.k8s_cluster_ver }} 80 | k8s_package_ver: "0.30.8" 81 | 82 | client_go_0_31: 83 | needs: prepare 84 | strategy: 85 | fail-fast: false 86 | # A matrix can produce no more than 256 elements, 87 | # so testing too many k8s versions is unfeasible. 88 | matrix: 89 | example_program: ${{ fromJson(needs.prepare.outputs.dirs) }} 90 | k8s_cluster_ver: 91 | - "1.28.9" 92 | - "1.29.12" 93 | - "1.30.8" 94 | - "1.31.4" 95 | uses: ./.github/workflows/test-one.yml 96 | with: 97 | example_program: ${{ matrix.example_program }} 98 | k8s_cluster_ver: ${{ matrix.k8s_cluster_ver }} 99 | k8s_package_ver: "0.31.4" 100 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | go.sum 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CUR_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | 3 | MINI_PROGRAMS_DIRS := $(shell find $(CUR_DIR) -type f -name go.mod -exec dirname {} \; | xargs -n 1 basename | sort -u) 4 | 5 | 6 | .PHONY: test-% 7 | test-%: 8 | @echo "\033[0;32m-- Test $*\033[0m" 9 | @cd ${CUR_DIR}/$* && make test && echo "\t--- PASS" || echo "\t--- FAILED" 10 | 11 | .PHONY: test-all 12 | test-all: $(addprefix test-, $(MINI_PROGRAMS_DIRS)) 13 | @echo "\033[0;32mDone all!\033[0m" 14 | 15 | .PHONY: go-mod-tidy-% 16 | go-mod-tidy-%: 17 | @cd ${CUR_DIR}/$* && make go-mod-tidy 18 | 19 | .PHONY: go-mod-tidy-all 20 | go-mod-tidy-all: $(addprefix go-mod-tidy-, $(MINI_PROGRAMS_DIRS)) 21 | @echo "\033[0;32mDone all!\033[0m" 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kubernetes client-go examples 2 | 3 | ```diff 4 | ! Support development of this project > patreon.com/iximiuz 5 | ``` 6 | 7 | A collection of mini-programs demonstrating various [client-go](https://github.com/kubernetes/client-go) use cases augmented by a [preconfigured online development environment](https://labs.iximiuz.com/playgrounds/k8s-client-go/). Inspired by [client-go/examples](https://github.com/kubernetes/client-go/tree/master/examples). 8 | 9 | The intention is to test a (more or less) fresh version of Go and `k8s.io` packages against the [currently maintained Kubernetes release branches](https://kubernetes.io/releases/). 10 | 11 | What is tested at the moment: 12 | 13 | - `go 1.22.10` 14 | - `k8s.io/client-go 0.28.14 0.29.12 0.30.8 0.31.4` (maintained release branches) 15 | - `Kubernetes 1.28.9 1.29.12 1.30.8 1.31.4` (best-effort match with versions supported by `kind`) 16 | 17 | ## Setup 18 | 19 | Most examples expect at least two Kubernetes clusters - `shared1` and `shared2`. 20 | 21 | ```bash 22 | curl -sLS https://get.arkade.dev | sudo sh 23 | arkade get kind kubectl 24 | 25 | kind create cluster --name shared1 26 | kind create cluster --name shared2 27 | ``` 28 | 29 | ## Run 30 | 31 | Oversimplified (for now): 32 | 33 | ```bash 34 | cd 35 | make test 36 | 37 | # or from the root folder: 38 | make test-all 39 | ``` 40 | 41 | ## TODO 42 | 43 | - Add more assertions to mini-programs 44 | - Examples to be covered 45 | - setting API request timeout 46 | - configuring API request throttling 47 | - `delete` 48 | - `delete collection` 49 | - `list` filtration 50 | - `watch` filtration 51 | - `informer` filtration 52 | - `patch` with different strategies 53 | - `Server Side Apply` (SSA) 54 | - working with subresources 55 | - `ownerReference` (one and many) 56 | - optimistic locking 57 | - https://stackoverflow.com/questions/56115197/how-to-idiomatically-fill-empty-fields-with-default-values-for-kubernetes-api-ob 58 | 59 | 60 | ## Contribution 61 | 62 | Contributions are always welcome! Want to participate but don't know where to start? The TODO list above could give you some ideas. 63 | Before jumping to the code, please create an issue describing the addition/change first. This will allow me to coordinate the effort 64 | and make sure multiple people don't work on the same task. 65 | -------------------------------------------------------------------------------- /cli-runtime-flags/.main.go.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iximiuz/client-go-examples/610e4f2234f628edaeafbfe7fc7663c0020d8b46/cli-runtime-flags/.main.go.swp -------------------------------------------------------------------------------- /cli-runtime-flags/Makefile: -------------------------------------------------------------------------------- 1 | CUR_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | 3 | 4 | .PHONY: test 5 | test: go-mod-tidy 6 | go run ${CUR_DIR}/main.go 7 | 8 | .PHONY: go-mod-tidy 9 | go-mod-tidy: 10 | cd ${CUR_DIR} && go mod tidy -------------------------------------------------------------------------------- /cli-runtime-flags/README.md: -------------------------------------------------------------------------------- 1 | # Simple CLI tool shows how to use cli-runtime package for common flag handling -------------------------------------------------------------------------------- /cli-runtime-flags/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iximiuz/client-go-examples/cli-runtime-flags 2 | 3 | go 1.22.3 4 | 5 | require ( 6 | k8s.io/cli-runtime v0.30.1 7 | ) 8 | -------------------------------------------------------------------------------- /cli-runtime-flags/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | "k8s.io/cli-runtime/pkg/genericclioptions" 8 | ) 9 | 10 | // go run main.go --help 11 | // go run main.go --kubeconfig /foo/bar (panic: stat /foo/bar: no such file or directory) 12 | // go run main.go --context shared1 13 | // go run main.go --context shared2 14 | 15 | func main() { 16 | configFlags := genericclioptions.NewConfigFlags(true) 17 | 18 | cmd := &cobra.Command{ 19 | Use: "kubectl (well, almost)", 20 | Run: func(cmd *cobra.Command, args []string) { 21 | // Interesting methods: 22 | // - ToRawKubeConfigLoader (returns clientcmd.ClientConfig) 23 | // - ToRESTConfig 24 | // - ToRESTMapper 25 | // - ToDiscoveryClient 26 | 27 | kubeconfig, err := configFlags.ToRawKubeConfigLoader().RawConfig() 28 | if err != nil { 29 | panic(err.Error()) 30 | } 31 | for name := range kubeconfig.Contexts { 32 | fmt.Printf("Found context %s\n", name) 33 | } 34 | 35 | restconfig, err := configFlags.ToRESTConfig() 36 | if err != nil { 37 | panic(err.Error()) 38 | } 39 | fmt.Println("Cluster host", restconfig.Host) 40 | 41 | client, err := configFlags.ToDiscoveryClient() 42 | if err != nil { 43 | panic(err.Error()) 44 | } 45 | 46 | ver, err := client.ServerVersion() 47 | if err != nil { 48 | panic(err.Error()) 49 | } 50 | 51 | fmt.Println(ver.String()) 52 | }, 53 | } 54 | configFlags.AddFlags(cmd.PersistentFlags()) 55 | 56 | if err := cmd.Execute(); err != nil { 57 | panic(err) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /cli-runtime-printers/Makefile: -------------------------------------------------------------------------------- 1 | CUR_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | 3 | 4 | .PHONY: test 5 | test: go-mod-tidy 6 | go run ${CUR_DIR}/main.go 7 | 8 | .PHONY: go-mod-tidy 9 | go-mod-tidy: 10 | cd ${CUR_DIR} && go mod tidy -------------------------------------------------------------------------------- /cli-runtime-printers/README.md: -------------------------------------------------------------------------------- 1 | # Different ways to print out Kubernetes objects - as YAML, as JSON, as Table, as JSONPath, etc. 2 | -------------------------------------------------------------------------------- /cli-runtime-printers/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iximiuz/client-go-examples/cli-runtime-printers 2 | 3 | go 1.22.10 4 | 5 | require ( 6 | k8s.io/api v0.30.1 7 | k8s.io/cli-runtime v0.30.1 8 | k8s.io/client-go v0.30.1 9 | ) 10 | -------------------------------------------------------------------------------- /cli-runtime-printers/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | corev1 "k8s.io/api/core/v1" 8 | "k8s.io/cli-runtime/pkg/printers" 9 | "k8s.io/client-go/kubernetes/scheme" 10 | ) 11 | 12 | func main() { 13 | obj := &corev1.ConfigMap{ 14 | Data: map[string]string{"foo": "bar"}, 15 | } 16 | obj.Name = "my-cm" 17 | 18 | // YAML 19 | fmt.Println("# YAML ConfigMap representation") 20 | printr := printers.NewTypeSetter(scheme.Scheme).ToPrinter(&printers.YAMLPrinter{}) 21 | if err := printr.PrintObj(obj, os.Stdout); err != nil { 22 | panic(err.Error()) 23 | } 24 | 25 | fmt.Println() 26 | 27 | // JSON 28 | fmt.Println("# JSON ConfigMap representation") 29 | printr = printers.NewTypeSetter(scheme.Scheme).ToPrinter(&printers.JSONPrinter{}) 30 | if err := printr.PrintObj(obj, os.Stdout); err != nil { 31 | panic(err.Error()) 32 | } 33 | 34 | fmt.Println() 35 | 36 | // Table (human-readable) 37 | fmt.Println("# Table ConfigMap representation") 38 | printr = printers.NewTypeSetter(scheme.Scheme).ToPrinter(printers.NewTablePrinter(printers.PrintOptions{})) 39 | if err := printr.PrintObj(obj, os.Stdout); err != nil { 40 | panic(err.Error()) 41 | } 42 | 43 | fmt.Println() 44 | 45 | // JSONPath 46 | fmt.Println("# ConfigMap.data.foo") 47 | printr, err := printers.NewJSONPathPrinter("{.data.foo}") 48 | if err != nil { 49 | panic(err.Error()) 50 | } 51 | 52 | printr = printers.NewTypeSetter(scheme.Scheme).ToPrinter(printr) 53 | if err := printr.PrintObj(obj, os.Stdout); err != nil { 54 | panic(err.Error()) 55 | } 56 | 57 | fmt.Println() 58 | 59 | // Name-only 60 | fmt.Println("# /") 61 | printr = printers.NewTypeSetter(scheme.Scheme).ToPrinter(&printers.NamePrinter{}) 62 | if err := printr.PrintObj(obj, os.Stdout); err != nil { 63 | panic(err.Error()) 64 | } 65 | 66 | fmt.Println() 67 | } 68 | -------------------------------------------------------------------------------- /cli-runtime-resources-from-cluster/Makefile: -------------------------------------------------------------------------------- 1 | CUR_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | 3 | 4 | .PHONY: test 5 | test: go-mod-tidy 6 | go run ${CUR_DIR}/main.go -n kube-system svc 7 | 8 | .PHONY: go-mod-tidy 9 | go-mod-tidy: 10 | cd ${CUR_DIR} && go mod tidy -------------------------------------------------------------------------------- /cli-runtime-resources-from-cluster/README.md: -------------------------------------------------------------------------------- 1 | # Simple tool to look up (and print out) Kubernetes objects by resource(s) and name(s) 2 | 3 | ```bash 4 | go run main.go --help 5 | go run main.go po 6 | go run main.go pod 7 | go run main.go pods 8 | go run main.go services,deployments 9 | go run main.go --namespace=default service/kubernetes 10 | ``` 11 | -------------------------------------------------------------------------------- /cli-runtime-resources-from-cluster/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iximiuz/client-go-examples/cli-runtime-resources-from-cluster 2 | 3 | go 1.22.10 4 | 5 | require ( 6 | github.com/spf13/cobra v1.7.0 7 | k8s.io/cli-runtime v0.30.1 8 | k8s.io/client-go v0.30.1 9 | ) 10 | -------------------------------------------------------------------------------- /cli-runtime-resources-from-cluster/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/cobra" 7 | "k8s.io/cli-runtime/pkg/genericclioptions" 8 | "k8s.io/cli-runtime/pkg/resource" 9 | "k8s.io/cli-runtime/pkg/printers" 10 | "k8s.io/client-go/kubernetes/scheme" 11 | ) 12 | 13 | // go run main.go --help 14 | // go run main.go po 15 | // go run main.go pod 16 | // go run main.go pods 17 | // go run main.go services,deployments 18 | // go run main.go --namespace=default service/kubernetes 19 | // go run main.go --namespace default service kubernetes 20 | 21 | func main() { 22 | configFlags := genericclioptions.NewConfigFlags(true) 23 | 24 | cmd := &cobra.Command{ 25 | Use: "kubectl (well, almost)", 26 | Args: cobra.MinimumNArgs(1), 27 | Run: func(cmd *cobra.Command, args []string) { 28 | builder := resource.NewBuilder(configFlags) 29 | 30 | namespace := "" 31 | if configFlags.Namespace != nil { 32 | namespace = *configFlags.Namespace 33 | } 34 | 35 | obj, err := builder. 36 | WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). 37 | NamespaceParam(namespace). 38 | DefaultNamespace(). 39 | ResourceTypeOrNameArgs(true, args...). 40 | Do(). 41 | Object() 42 | if err != nil { 43 | panic(err.Error()) 44 | } 45 | 46 | printr := printers.NewTypeSetter(scheme.Scheme).ToPrinter(&printers.YAMLPrinter{}) 47 | if err := printr.PrintObj(obj, os.Stdout); err != nil { 48 | panic(err.Error()) 49 | } 50 | }, 51 | } 52 | configFlags.AddFlags(cmd.PersistentFlags()) 53 | 54 | if err := cmd.Execute(); err != nil { 55 | panic(err) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /cli-runtime-resources-from-file/Makefile: -------------------------------------------------------------------------------- 1 | CUR_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | 3 | 4 | .PHONY: test 5 | test: go-mod-tidy 6 | go run ${CUR_DIR}/main.go resources.yaml 7 | 8 | .PHONY: go-mod-tidy 9 | go-mod-tidy: 10 | cd ${CUR_DIR} && go mod tidy -------------------------------------------------------------------------------- /cli-runtime-resources-from-file/README.md: -------------------------------------------------------------------------------- 1 | # Simple tool to read a JSON/YAML Kubernetes manifest and print out all the resources it contains 2 | 3 | ```bash 4 | go run main.go resources.yaml 5 | ``` 6 | -------------------------------------------------------------------------------- /cli-runtime-resources-from-file/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iximiuz/client-go-examples/cli-runtime-resources-from-file 2 | 3 | go 1.22.10 4 | 5 | require ( 6 | github.com/spf13/cobra v1.7.0 7 | k8s.io/cli-runtime v0.30.1 8 | k8s.io/client-go v0.30.1 9 | ) 10 | -------------------------------------------------------------------------------- /cli-runtime-resources-from-file/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/cobra" 7 | "k8s.io/cli-runtime/pkg/genericclioptions" 8 | "k8s.io/cli-runtime/pkg/printers" 9 | "k8s.io/cli-runtime/pkg/resource" 10 | "k8s.io/client-go/kubernetes/scheme" 11 | ) 12 | 13 | // go run main.go resources.yaml 14 | 15 | func main() { 16 | configFlags := genericclioptions.NewConfigFlags(true) 17 | 18 | cmd := &cobra.Command{ 19 | Use: "kubectl (well, almost)", 20 | Args: cobra.MinimumNArgs(1), 21 | Run: func(cmd *cobra.Command, args []string) { 22 | builder := resource.NewBuilder(configFlags) 23 | 24 | namespace := "" 25 | if configFlags.Namespace != nil { 26 | namespace = *configFlags.Namespace 27 | } 28 | enforceNamespace := namespace != "" 29 | 30 | printr := printers.NewTypeSetter(scheme.Scheme).ToPrinter(&printers.YAMLPrinter{}) 31 | 32 | err := builder. 33 | WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). 34 | NamespaceParam(namespace). 35 | DefaultNamespace(). 36 | FilenameParam(enforceNamespace, &resource.FilenameOptions{Filenames: args}). 37 | Do(). 38 | Visit(func(info *resource.Info, err error) error { 39 | if err != nil { 40 | return err 41 | } 42 | 43 | return printr.PrintObj(info.Object, os.Stdout) 44 | }) 45 | if err != nil { 46 | panic(err.Error()) 47 | } 48 | 49 | }, 50 | } 51 | configFlags.AddFlags(cmd.PersistentFlags()) 52 | 53 | if err := cmd.Execute(); err != nil { 54 | panic(err) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /cli-runtime-resources-from-file/resources.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: test 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: test 9 | template: 10 | metadata: 11 | labels: 12 | app: test 13 | spec: 14 | containers: 15 | - name: app 16 | image: nginx 17 | imagePullPolicy: IfNotPresent 18 | ports: 19 | - name: http-80 20 | containerPort: 80 21 | protocol: TCP 22 | --- 23 | apiVersion: v1 24 | kind: Service 25 | metadata: 26 | name: test 27 | namespace: default 28 | spec: 29 | type: ClusterIP 30 | ports: 31 | - port: 80 32 | protocol: TCP 33 | targetPort: 80 34 | selector: 35 | app: test 36 | -------------------------------------------------------------------------------- /convert-unstructured-typed/Makefile: -------------------------------------------------------------------------------- 1 | CUR_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | 3 | 4 | .PHONY: test 5 | test: go-mod-tidy 6 | go run ${CUR_DIR}/main.go 7 | 8 | .PHONY: go-mod-tidy 9 | go-mod-tidy: 10 | cd ${CUR_DIR} && go mod tidy -------------------------------------------------------------------------------- /convert-unstructured-typed/README.md: -------------------------------------------------------------------------------- 1 | # How to convert unstructured objects to typed and vice versa 2 | 3 | This mini-program demonstates usage of: 4 | 5 | - `k8s.io/apimachinery/pkg/runtime.UnstructuredConverter` -------------------------------------------------------------------------------- /convert-unstructured-typed/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iximiuz/client-go-examples/convert-unstructured-typed 2 | 3 | go 1.22.10 4 | 5 | require ( 6 | k8s.io/api v0.30.1 7 | k8s.io/apimachinery v0.30.1 8 | ) 9 | -------------------------------------------------------------------------------- /convert-unstructured-typed/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | 6 | corev1 "k8s.io/api/core/v1" 7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | ) 10 | 11 | func main() { 12 | uConfigMap := unstructured.Unstructured{ 13 | Object: map[string]interface{}{ 14 | "apiVersion": "v1", 15 | "kind": "ConfigMap", 16 | "metadata": map[string]interface{}{ 17 | "creationTimestamp": nil, 18 | "namespace": "default", 19 | "name": "my-configmap", 20 | }, 21 | "data": map[string]interface{}{ 22 | "foo": "bar", 23 | }, 24 | }, 25 | } 26 | 27 | // Unstructured -> Typed 28 | var tConfigMap corev1.ConfigMap 29 | err := runtime.DefaultUnstructuredConverter.FromUnstructured(uConfigMap.Object, &tConfigMap) 30 | if err != nil { 31 | panic(err.Error()) 32 | } 33 | if tConfigMap.GetName() != "my-configmap" { 34 | panic("Typed config map has unexpected data") 35 | } 36 | 37 | // Typed -> Unstructured 38 | object, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&tConfigMap) 39 | if err != nil { 40 | panic(err.Error()) 41 | } 42 | if !reflect.DeepEqual(unstructured.Unstructured{Object: object}, uConfigMap) { 43 | panic("Unstructured config map has unexpected data") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /crud-dynamic-simple/Makefile: -------------------------------------------------------------------------------- 1 | CUR_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | 3 | 4 | .PHONY: test 5 | test: go-mod-tidy 6 | go run ${CUR_DIR}/main.go 7 | 8 | .PHONY: go-mod-tidy 9 | go-mod-tidy: 10 | cd ${CUR_DIR} && go mod tidy -------------------------------------------------------------------------------- /crud-dynamic-simple/README.md: -------------------------------------------------------------------------------- 1 | # Simple CRUD operations using dynamic Kubernetes client -------------------------------------------------------------------------------- /crud-dynamic-simple/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iximiuz/client-go-examples/crud-dynamic-simple 2 | 3 | go 1.22.10 4 | 5 | require ( 6 | k8s.io/apimachinery v0.30.1 7 | k8s.io/client-go v0.30.1 8 | ) 9 | -------------------------------------------------------------------------------- /crud-dynamic-simple/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path" 8 | "reflect" 9 | 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 12 | "k8s.io/apimachinery/pkg/runtime/schema" 13 | "k8s.io/client-go/dynamic" 14 | "k8s.io/client-go/tools/clientcmd" 15 | ) 16 | 17 | func main() { 18 | home, err := os.UserHomeDir() 19 | if err != nil { 20 | panic(err) 21 | } 22 | 23 | config, err := clientcmd.BuildConfigFromFlags("", path.Join(home, ".kube/config")) 24 | if err != nil { 25 | panic(err.Error()) 26 | } 27 | 28 | client, err := dynamic.NewForConfig(config) 29 | if err != nil { 30 | panic(err.Error()) 31 | } 32 | 33 | namespace := "default" 34 | res := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "configmaps"} 35 | 36 | desired := &unstructured.Unstructured{ 37 | Object: map[string]interface{}{ 38 | "apiVersion": "v1", 39 | "kind": "ConfigMap", 40 | "metadata": map[string]interface{}{ 41 | "namespace": namespace, 42 | "generateName": "crud-dynamic-simple-", 43 | }, 44 | "data": map[string]interface{}{ 45 | "foo": "bar", 46 | }, 47 | }, 48 | } 49 | 50 | // Create 51 | created, err := client. 52 | Resource(res). 53 | Namespace(namespace). 54 | Create(context.Background(), desired, metav1.CreateOptions{}) 55 | if err != nil { 56 | panic(err.Error()) 57 | } 58 | 59 | fmt.Printf("Created ConfigMap %s/%s\n", namespace, created.GetName()) 60 | 61 | data, _, _ := unstructured.NestedStringMap(created.Object, "data") 62 | if !reflect.DeepEqual(map[string]string{"foo": "bar"}, data) { 63 | panic("Created ConfigMap has unexpected data") 64 | } 65 | 66 | // Read 67 | read, err := client. 68 | Resource(res). 69 | Namespace(namespace). 70 | Get( 71 | context.Background(), 72 | created.GetName(), 73 | metav1.GetOptions{}, 74 | ) 75 | if err != nil { 76 | panic(err.Error()) 77 | } 78 | 79 | fmt.Printf("Read ConfigMap %s/%s\n", namespace, read.GetName()) 80 | 81 | data, _, _ = unstructured.NestedStringMap(read.Object, "data") 82 | if !reflect.DeepEqual(map[string]string{"foo": "bar"}, data) { 83 | panic("Read ConfigMap has unexpected data") 84 | } 85 | 86 | // Update 87 | unstructured.SetNestedField(read.Object, "qux", "data", "foo") 88 | updated, err := client. 89 | Resource(res). 90 | Namespace(namespace). 91 | Update( 92 | context.Background(), 93 | read, 94 | metav1.UpdateOptions{}, 95 | ) 96 | if err != nil { 97 | panic(err.Error()) 98 | } 99 | 100 | fmt.Printf("Updated ConfigMap %s/%s\n", namespace, updated.GetName()) 101 | 102 | data, _, _ = unstructured.NestedStringMap(updated.Object, "data") 103 | if !reflect.DeepEqual(map[string]string{"foo": "qux"}, data) { 104 | panic("Updated ConfigMap has unexpected data") 105 | } 106 | 107 | // Delete 108 | err = client. 109 | Resource(res). 110 | Namespace(namespace). 111 | Delete( 112 | context.Background(), 113 | created.GetName(), 114 | metav1.DeleteOptions{}, 115 | ) 116 | if err != nil { 117 | panic(err.Error()) 118 | } 119 | fmt.Printf("Deleted ConfigMap %s/%s\n", namespace, created.GetName()) 120 | } 121 | -------------------------------------------------------------------------------- /crud-typed-simple/Makefile: -------------------------------------------------------------------------------- 1 | CUR_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | 3 | 4 | .PHONY: test 5 | test: go-mod-tidy 6 | go run ${CUR_DIR}/main.go 7 | 8 | .PHONY: go-mod-tidy 9 | go-mod-tidy: 10 | cd ${CUR_DIR} && go mod tidy -------------------------------------------------------------------------------- /crud-typed-simple/README.md: -------------------------------------------------------------------------------- 1 | # Simple CRUD operations using typed Kubernetes Clientset -------------------------------------------------------------------------------- /crud-typed-simple/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iximiuz/client-go-examples/crud-typed-simple 2 | 3 | go 1.22.10 4 | 5 | require ( 6 | k8s.io/api v0.30.1 7 | k8s.io/apimachinery v0.30.1 8 | k8s.io/client-go v0.30.1 9 | ) 10 | -------------------------------------------------------------------------------- /crud-typed-simple/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path" 8 | "reflect" 9 | 10 | corev1 "k8s.io/api/core/v1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/client-go/kubernetes" 13 | "k8s.io/client-go/tools/clientcmd" 14 | ) 15 | 16 | func main() { 17 | home, err := os.UserHomeDir() 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | config, err := clientcmd.BuildConfigFromFlags("", path.Join(home, ".kube/config")) 23 | if err != nil { 24 | panic(err.Error()) 25 | } 26 | 27 | client, err := kubernetes.NewForConfig(config) 28 | if err != nil { 29 | panic(err.Error()) 30 | } 31 | 32 | namespace := "default" 33 | 34 | desired := corev1.ConfigMap{Data: map[string]string{"foo": "bar"}} 35 | desired.Namespace = namespace 36 | desired.GenerateName = "crud-typed-simple-" 37 | 38 | // Create 39 | created, err := client. 40 | CoreV1(). 41 | ConfigMaps(namespace). 42 | Create( 43 | context.Background(), 44 | &desired, 45 | metav1.CreateOptions{}, 46 | ) 47 | if err != nil { 48 | panic(err.Error()) 49 | } 50 | 51 | fmt.Printf("Created ConfigMap %s/%s\n", namespace, created.GetName()) 52 | 53 | if !reflect.DeepEqual(created.Data, desired.Data) { 54 | panic("Created ConfigMap has unexpected data") 55 | } 56 | 57 | // Read 58 | read, err := client. 59 | CoreV1(). 60 | ConfigMaps(namespace). 61 | Get( 62 | context.Background(), 63 | created.GetName(), 64 | metav1.GetOptions{}, 65 | ) 66 | if err != nil { 67 | panic(err.Error()) 68 | } 69 | 70 | fmt.Printf("Read ConfigMap %s/%s\n", namespace, read.GetName()) 71 | 72 | if !reflect.DeepEqual(read.Data, desired.Data) { 73 | panic("Read ConfigMap has unexpected data") 74 | } 75 | 76 | // Update 77 | read.Data["foo"] = "qux" 78 | updated, err := client. 79 | CoreV1(). 80 | ConfigMaps(namespace). 81 | Update( 82 | context.Background(), 83 | read, 84 | metav1.UpdateOptions{}, 85 | ) 86 | if err != nil { 87 | panic(err.Error()) 88 | } 89 | 90 | fmt.Printf("Updated ConfigMap %s/%s\n", namespace, updated.GetName()) 91 | 92 | if !reflect.DeepEqual(updated.Data, read.Data) { 93 | panic("Updated ConfigMap has unexpected data") 94 | } 95 | 96 | // Delete 97 | err = client. 98 | CoreV1(). 99 | ConfigMaps(namespace). 100 | Delete( 101 | context.Background(), 102 | created.GetName(), 103 | metav1.DeleteOptions{}, 104 | ) 105 | if err != nil { 106 | panic(err.Error()) 107 | } 108 | 109 | fmt.Printf("Deleted ConfigMap %s/%s\n", namespace, created.GetName()) 110 | } 111 | -------------------------------------------------------------------------------- /error-handling/Makefile: -------------------------------------------------------------------------------- 1 | CUR_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | 3 | 4 | .PHONY: test 5 | test: go-mod-tidy 6 | go run ${CUR_DIR}/main.go 7 | 8 | .PHONY: go-mod-tidy 9 | go-mod-tidy: 10 | cd ${CUR_DIR} && go mod tidy -------------------------------------------------------------------------------- /error-handling/README.md: -------------------------------------------------------------------------------- 1 | # Various API error handling examples -------------------------------------------------------------------------------- /error-handling/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iximiuz/client-go-examples/error-handling 2 | 3 | go 1.22.10 4 | 5 | require ( 6 | k8s.io/api v0.30.1 7 | k8s.io/apimachinery v0.30.1 8 | k8s.io/client-go v0.30.1 9 | ) 10 | -------------------------------------------------------------------------------- /error-handling/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path" 8 | 9 | corev1 "k8s.io/api/core/v1" 10 | "k8s.io/apimachinery/pkg/api/errors" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/client-go/kubernetes" 13 | "k8s.io/client-go/tools/clientcmd" 14 | ) 15 | 16 | func main() { 17 | home, err := os.UserHomeDir() 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | config, err := clientcmd.BuildConfigFromFlags("", path.Join(home, ".kube/config")) 23 | if err != nil { 24 | panic(err.Error()) 25 | } 26 | 27 | client, err := kubernetes.NewForConfig(config) 28 | if err != nil { 29 | panic(err.Error()) 30 | } 31 | 32 | namespace := "default" 33 | 34 | // ERR_NOT_FOUND 35 | _, err = client. 36 | CoreV1(). 37 | ConfigMaps(namespace). 38 | Get( 39 | context.Background(), 40 | "this_name_definitely_does_not_exist", 41 | metav1.GetOptions{}, 42 | ) 43 | if err == nil { 44 | panic("ERR_NOT_FOUND expected") 45 | } 46 | if !errors.IsNotFound(err) { 47 | panic(err.Error()) 48 | } 49 | 50 | desired := corev1.ConfigMap{Data: map[string]string{"foo": "bar"}} 51 | desired.Namespace = namespace 52 | desired.GenerateName = "crud-typed-simple-" 53 | 54 | // Create 55 | created, err := client. 56 | CoreV1(). 57 | ConfigMaps(namespace). 58 | Create( 59 | context.Background(), 60 | &desired, 61 | metav1.CreateOptions{}, 62 | ) 63 | if err != nil { 64 | panic(err.Error()) 65 | } 66 | fmt.Printf("Created ConfigMap %s/%s\n", namespace, created.GetName()) 67 | 68 | // ERR_ALREADY_EXISTS 69 | duplicate := corev1.ConfigMap{} 70 | desired.Namespace = namespace 71 | duplicate.Name = created.Name 72 | _, err = client. 73 | CoreV1(). 74 | ConfigMaps(namespace). 75 | Create( 76 | context.Background(), 77 | &duplicate, 78 | metav1.CreateOptions{}, 79 | ) 80 | if err == nil { 81 | panic("ERR_ALREADY_EXISTS expected") 82 | } 83 | if !errors.IsAlreadyExists(err) { 84 | panic(err.Error()) 85 | } 86 | 87 | // Update 88 | created.Data["qux"] = "abc" 89 | updated, err := client. 90 | CoreV1(). 91 | ConfigMaps(namespace). 92 | Update( 93 | context.Background(), 94 | created, 95 | metav1.UpdateOptions{}, 96 | ) 97 | if err != nil { 98 | panic(err.Error()) 99 | } 100 | fmt.Printf("Updated ConfigMap %s/%s\n", namespace, updated.GetName()) 101 | 102 | // ERR_CONFLICT 103 | created.Data["baz"] = "def" 104 | _, err = client. 105 | CoreV1(). 106 | ConfigMaps(namespace). 107 | Update( 108 | context.Background(), 109 | created, 110 | metav1.UpdateOptions{}, 111 | ) 112 | if err == nil { 113 | panic("ERR_CONFLICT expected") 114 | } 115 | if !errors.IsConflict(err) { 116 | panic(err.Error()) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /field-selectors/Makefile: -------------------------------------------------------------------------------- 1 | CUR_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | 3 | 4 | .PHONY: test 5 | test: go-mod-tidy 6 | go run ${CUR_DIR}/main.go 7 | 8 | .PHONY: go-mod-tidy 9 | go-mod-tidy: 10 | cd ${CUR_DIR} && go mod tidy -------------------------------------------------------------------------------- /field-selectors/README.md: -------------------------------------------------------------------------------- 1 | # How to construct field selectors -------------------------------------------------------------------------------- /field-selectors/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iximiuz/client-go-examples/field-selectors 2 | 3 | go 1.22.10 4 | 5 | require ( 6 | k8s.io/apimachinery v0.30.1 7 | ) 8 | -------------------------------------------------------------------------------- /field-selectors/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "k8s.io/apimachinery/pkg/fields" 7 | ) 8 | 9 | func main() { 10 | flds := fields.Set{"foo": "bar", "baz": "qux"} 11 | 12 | // Selector matching existing field set. 13 | sel := fields.SelectorFromSet(flds) 14 | if sel.Matches(flds) { 15 | fmt.Printf("Selector %v matched field set %v\n", sel, flds) 16 | } else { 17 | panic("Selector should have matched field set") 18 | } 19 | 20 | // f==v selector. 21 | sel = fields.OneTermEqualSelector("foo", "bar") 22 | if sel.Matches(flds) { 23 | fmt.Printf("Selector %v matched field set %v\n", sel, flds) 24 | } else { 25 | panic("Selector should have matched field set") 26 | } 27 | 28 | // f!=v selector. 29 | sel = fields.OneTermNotEqualSelector("qux", "abc") 30 | if sel.Matches(flds) { 31 | fmt.Printf("Selector %v matched field set %v\n", sel, flds) 32 | } else { 33 | panic("Selector should have not matched field set") 34 | } 35 | 36 | // f1=v1,f2=v2 37 | sel = fields.AndSelectors( 38 | fields.OneTermEqualSelector("foo", "bar"), 39 | fields.OneTermEqualSelector("baz", "qux"), 40 | ) 41 | if sel.Matches(flds) { 42 | fmt.Printf("Selector %v matched field set %v\n", sel, flds) 43 | } else { 44 | panic("Selector should have not matched field set") 45 | } 46 | 47 | // Selector from string expression. 48 | sel, err := fields.ParseSelector("foo==bar") 49 | if err != nil { 50 | panic(err.Error()) 51 | } 52 | if sel.Matches(flds) { 53 | fmt.Printf("Selector %v matched field set %v\n", sel, flds) 54 | } else { 55 | panic("Selector should have matched field set") 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.22.10 2 | 3 | use ( 4 | ./cli-runtime-flags 5 | ./cli-runtime-printers 6 | ./cli-runtime-resources-from-cluster 7 | ./cli-runtime-resources-from-file 8 | ./convert-unstructured-typed 9 | ./crud-dynamic-simple 10 | ./crud-typed-simple 11 | ./error-handling 12 | ./field-selectors 13 | ./informer-dynamic-simple 14 | ./informer-typed-simple 15 | ./kubeconfig-default-context 16 | ./kubeconfig-from-yaml 17 | ./kubeconfig-list-contexts 18 | ./kubeconfig-overridden-context 19 | ./label-selectors 20 | ./list-typed-simple 21 | ./patch-add-ephemeral-container 22 | ./retry-on-conflict 23 | ./serialize-typed-json 24 | ./serialize-typed-yaml 25 | ./serialize-unstructured-json 26 | ./serialize-unstructured-yaml 27 | ./watch-typed-simple 28 | ./workqueue 29 | ) 30 | -------------------------------------------------------------------------------- /go.work.sum: -------------------------------------------------------------------------------- 1 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 2 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 4 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= 8 | github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 9 | github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= 10 | github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 11 | github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= 12 | github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 13 | github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= 14 | github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= 15 | github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= 16 | github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 17 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 18 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 19 | github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= 20 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 21 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 22 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 23 | github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= 24 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 25 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 26 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= 27 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 28 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 29 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 30 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 31 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 32 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 33 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 34 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 35 | github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= 36 | github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= 37 | github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= 38 | github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= 39 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 40 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 41 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 42 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 43 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 44 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 45 | github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= 46 | github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 47 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 48 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 49 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 50 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 51 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= 52 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 53 | github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= 54 | github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 55 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 56 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 57 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 58 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 59 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 60 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 61 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 62 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 63 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 64 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 65 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= 66 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= 67 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 68 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 69 | github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= 70 | github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= 71 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 72 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 73 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 74 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 75 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= 76 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= 77 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 78 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 79 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= 80 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 81 | github.com/onsi/ginkgo/v2 v2.9.4 h1:xR7vG4IXt5RWx6FfIjyAtsoMAtnc3C/rFXBBd2AjZwE= 82 | github.com/onsi/ginkgo/v2 v2.9.4/go.mod h1:gCQYp2Q+kSoIj7ykSVb9nskRSsR6PUj4AiLywzIhbKM= 83 | github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= 84 | github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= 85 | github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= 86 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 87 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 88 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 89 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 90 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 91 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 92 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 93 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 94 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 95 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 96 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 97 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 98 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 99 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 100 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 101 | github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= 102 | github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= 103 | go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= 104 | go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= 105 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= 106 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 107 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 108 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 109 | golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= 110 | golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= 111 | golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= 112 | golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= 113 | golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= 114 | golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 115 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= 116 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 117 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= 118 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 119 | golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= 120 | golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= 121 | golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= 122 | golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= 123 | golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= 124 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 125 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 126 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 127 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 128 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 129 | golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= 130 | golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= 131 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 132 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 133 | google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= 134 | google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 135 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 136 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 137 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 138 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 139 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 140 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 141 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 142 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 143 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 144 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 145 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 146 | k8s.io/api v0.28.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM= 147 | k8s.io/api v0.28.3/go.mod h1:MRCV/jr1dW87/qJnZ57U5Pak65LGmQVkKTzf3AtKFHc= 148 | k8s.io/api v0.30.1 h1:kCm/6mADMdbAxmIh0LBjS54nQBE+U4KmbCfIkF5CpJY= 149 | k8s.io/api v0.30.1/go.mod h1:ddbN2C0+0DIiPntan/bye3SW3PdwLa11/0yqwvuRrJM= 150 | k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A= 151 | k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8= 152 | k8s.io/apimachinery v0.30.1 h1:ZQStsEfo4n65yAdlGTfP/uSHMQSoYzU/oeEbkmF7P2U= 153 | k8s.io/apimachinery v0.30.1/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= 154 | k8s.io/cli-runtime v0.28.3 h1:lvuJYVkwCqHEvpS6KuTZsUVwPePFjBfSGvuaLl2SxzA= 155 | k8s.io/cli-runtime v0.28.3/go.mod h1:jeX37ZPjIcENVuXDDTskG3+FnVuZms5D9omDXS/2Jjc= 156 | k8s.io/cli-runtime v0.30.1/go.mod h1:zhHgbqI4J00pxb6gM3gJPVf2ysDjhQmQtnTxnMScab8= 157 | k8s.io/client-go v0.28.3 h1:2OqNb72ZuTZPKCl+4gTKvqao0AMOl9f3o2ijbAj3LI4= 158 | k8s.io/client-go v0.28.3/go.mod h1:LTykbBp9gsA7SwqirlCXBWtK0guzfhpoW4qSm7i9dxo= 159 | k8s.io/client-go v0.30.1 h1:uC/Ir6A3R46wdkgCV3vbLyNOYyCJ8oZnjtJGKfytl/Q= 160 | k8s.io/client-go v0.30.1/go.mod h1:wrAqLNs2trwiCH/wxxmT/x3hKVH9PuV0GGW0oDoHVqc= 161 | k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= 162 | k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= 163 | k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= 164 | k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 165 | k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ= 166 | k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= 167 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= 168 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= 169 | k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= 170 | k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 171 | k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= 172 | k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 173 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= 174 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 175 | sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 h1:XX3Ajgzov2RKUdc5jW3t5jwY7Bo7dcRm+tFxT+NfgY0= 176 | sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3/go.mod h1:9n16EZKMhXBNSiUC5kSdFQJkdH3zbxS/JoO619G1VAY= 177 | sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 h1:W6cLQc5pnqM7vh3b7HvGNfXrJ/xL6BDMS0v1V/HHg5U= 178 | sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3/go.mod h1:JWP1Fj0VWGHyw3YUPjXSQnRnrwezrZSrApfX5S0nIag= 179 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= 180 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= 181 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= 182 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= 183 | sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= 184 | sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= 185 | -------------------------------------------------------------------------------- /informer-dynamic-simple/Makefile: -------------------------------------------------------------------------------- 1 | CUR_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | 3 | 4 | .PHONY: test 5 | test: go-mod-tidy 6 | go run ${CUR_DIR}/main.go 7 | 8 | .PHONY: go-mod-tidy 9 | go-mod-tidy: 10 | cd ${CUR_DIR} && go mod tidy -------------------------------------------------------------------------------- /informer-dynamic-simple/README.md: -------------------------------------------------------------------------------- 1 | # Cached listing and watching unstructured Kubernetes objects using shared dynamic informer -------------------------------------------------------------------------------- /informer-dynamic-simple/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iximiuz/client-go-examples/informer-dynamic-simple 2 | 3 | go 1.22.10 4 | 5 | require ( 6 | k8s.io/apimachinery v0.30.1 7 | k8s.io/client-go v0.30.1 8 | ) 9 | -------------------------------------------------------------------------------- /informer-dynamic-simple/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path" 8 | "time" 9 | 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 12 | "k8s.io/apimachinery/pkg/labels" 13 | "k8s.io/apimachinery/pkg/runtime/schema" 14 | "k8s.io/apimachinery/pkg/util/rand" 15 | "k8s.io/client-go/dynamic" 16 | "k8s.io/client-go/dynamic/dynamicinformer" 17 | "k8s.io/client-go/tools/cache" 18 | "k8s.io/client-go/tools/clientcmd" 19 | ) 20 | 21 | var ( 22 | namespace = "default" 23 | label = "informer-dynamic-simple-" + rand.String(6) 24 | ConfigMapResource = schema.GroupVersionResource{ 25 | Group: "", 26 | Version: "v1", 27 | Resource: "configmaps", 28 | } 29 | ) 30 | 31 | func main() { 32 | home, err := os.UserHomeDir() 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | config, err := clientcmd.BuildConfigFromFlags("", path.Join(home, ".kube/config")) 38 | if err != nil { 39 | panic(err.Error()) 40 | } 41 | 42 | client, err := dynamic.NewForConfig(config) 43 | if err != nil { 44 | panic(err.Error()) 45 | } 46 | 47 | // Create one object before initializing the informer. 48 | first := createConfigMap(client) 49 | 50 | // Create a shared informer factory. 51 | // - A factory is essentially a struct keeping a map (type -> informer). 52 | // - 5*time.Second is a default resync period (for all informers). 53 | factory := dynamicinformer.NewDynamicSharedInformerFactory(client, 5*time.Second) 54 | 55 | // When informer is requested, the factory instantiates it and keeps the 56 | // the reference to it in the internal map before returning. 57 | dynamicInformer := factory.ForResource(ConfigMapResource) 58 | dynamicInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ 59 | AddFunc: func(obj interface{}) { 60 | cm := obj.(*unstructured.Unstructured) 61 | fmt.Printf("Informer event: ConfigMap ADDED %s/%s\n", cm.GetNamespace(), cm.GetName()) 62 | }, 63 | UpdateFunc: func(old, new interface{}) { 64 | cm := old.(*unstructured.Unstructured) 65 | fmt.Printf("Informer event: ConfigMap UPDATED %s/%s\n", cm.GetNamespace(), cm.GetName()) 66 | }, 67 | DeleteFunc: func(obj interface{}) { 68 | cm := obj.(*unstructured.Unstructured) 69 | fmt.Printf("Informer event: ConfigMap DELETED %s/%s\n", cm.GetNamespace(), cm.GetName()) 70 | }, 71 | }) 72 | 73 | ctx, cancel := context.WithCancel(context.Background()) 74 | defer cancel() 75 | 76 | // Start the informers' machinery. 77 | // - Start() starts every Informer requested before using a goroutine per informer. 78 | // - A started Informer will fetch ALL the ConfigMaps from all the namespaces 79 | // (using a lister) and trigger `AddFunc`` for each found ConfigMap object. 80 | // Use NewSharedInformerFactoryWithOptions() to make the lister fetch only 81 | // a filtered subset of objects. 82 | // - All ConfigMaps added, updated, or deleted after the informer has been synced 83 | // will trigger the corresponding callback call (using a watch). 84 | // - Every 5*time.Second the UpdateFunc callback will be called for every 85 | // previously fetched ConfigMap (so-called resync period). 86 | factory.Start(ctx.Done()) 87 | 88 | // factory.Start() releases the execution flow without waiting for all the 89 | // internal machinery to warm up. We use factory.WaitForCacheSync() here 90 | // to poll for cmInformer.Informer().HasSynced(). Essentially, it's just a 91 | // fancy way to write a while-loop checking HasSynced() flags for all the 92 | // registered informers with 100ms delay between iterations. 93 | for gvr, ok := range factory.WaitForCacheSync(ctx.Done()) { 94 | if !ok { 95 | panic(fmt.Sprintf("Failed to sync cache for resource %v", gvr)) 96 | } 97 | } 98 | 99 | // Search for the existing ConfigMap object using the label selector. 100 | selector, err := labels.Parse("example==" + label) 101 | if err != nil { 102 | panic(err.Error()) 103 | } 104 | list, err := dynamicInformer.Lister().List(selector) 105 | if err != nil { 106 | panic(err.Error()) 107 | } 108 | if len(list) != 1 { 109 | panic("expected ConfigMap not found") 110 | } 111 | 112 | // Create another object while watching. 113 | second := createConfigMap(client) 114 | 115 | // Delete config maps created by this test. 116 | deleteConfigMap(client, first) 117 | deleteConfigMap(client, second) 118 | 119 | // Stay for a couple more seconds to observe resyncs. 120 | time.Sleep(10 * time.Second) 121 | } 122 | 123 | func createConfigMap(client dynamic.Interface) *unstructured.Unstructured { 124 | cm := &unstructured.Unstructured{ 125 | Object: map[string]interface{}{ 126 | "apiVersion": "v1", 127 | "kind": "ConfigMap", 128 | "metadata": map[string]interface{}{ 129 | "namespace": namespace, 130 | "generateName": "informer-dynamic-simple-", 131 | "labels": map[string]interface{}{ 132 | "example": label, 133 | }, 134 | }, 135 | "data": map[string]interface{}{ 136 | "foo": "bar", 137 | }, 138 | }, 139 | } 140 | 141 | cm, err := client. 142 | Resource(ConfigMapResource). 143 | Namespace(namespace). 144 | Create(context.Background(), cm, metav1.CreateOptions{}) 145 | if err != nil { 146 | panic(err.Error()) 147 | } 148 | 149 | fmt.Printf("Created ConfigMap %s/%s\n", cm.GetNamespace(), cm.GetName()) 150 | return cm 151 | } 152 | 153 | func deleteConfigMap(client dynamic.Interface, cm *unstructured.Unstructured) { 154 | err := client. 155 | Resource(ConfigMapResource). 156 | Namespace(cm.GetNamespace()). 157 | Delete(context.Background(), cm.GetName(), metav1.DeleteOptions{}) 158 | if err != nil { 159 | panic(err.Error()) 160 | } 161 | 162 | fmt.Printf("Deleted ConfigMap %s/%s\n", cm.GetNamespace(), cm.GetName()) 163 | } 164 | -------------------------------------------------------------------------------- /informer-typed-simple/Makefile: -------------------------------------------------------------------------------- 1 | CUR_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | 3 | 4 | .PHONY: test 5 | test: go-mod-tidy 6 | go run ${CUR_DIR}/main.go 7 | 8 | .PHONY: go-mod-tidy 9 | go-mod-tidy: 10 | cd ${CUR_DIR} && go mod tidy -------------------------------------------------------------------------------- /informer-typed-simple/README.md: -------------------------------------------------------------------------------- 1 | # Cached listing and watching Kubernetes objects using shared informer -------------------------------------------------------------------------------- /informer-typed-simple/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iximiuz/client-go-examples/informer-typed-simple 2 | 3 | go 1.22.10 4 | 5 | require ( 6 | k8s.io/api v0.30.1 7 | k8s.io/apimachinery v0.30.1 8 | k8s.io/client-go v0.30.1 9 | ) 10 | -------------------------------------------------------------------------------- /informer-typed-simple/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path" 8 | "time" 9 | 10 | corev1 "k8s.io/api/core/v1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/apimachinery/pkg/labels" 13 | "k8s.io/apimachinery/pkg/util/rand" 14 | "k8s.io/client-go/informers" 15 | "k8s.io/client-go/kubernetes" 16 | "k8s.io/client-go/tools/cache" 17 | "k8s.io/client-go/tools/clientcmd" 18 | ) 19 | 20 | var ( 21 | namespace = "default" 22 | label = "informer-typed-simple-" + rand.String(6) 23 | ) 24 | 25 | func main() { 26 | home, err := os.UserHomeDir() 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | config, err := clientcmd.BuildConfigFromFlags("", path.Join(home, ".kube/config")) 32 | if err != nil { 33 | panic(err.Error()) 34 | } 35 | 36 | client, err := kubernetes.NewForConfig(config) 37 | if err != nil { 38 | panic(err.Error()) 39 | } 40 | 41 | // Create one object before initializing the informer. 42 | first := createConfigMap(client) 43 | 44 | // Create a shared informer factory. 45 | // - A factory is essentially a struct keeping a map (type -> informer). 46 | // - 5*time.Second is a default resync period (for all informers). 47 | factory := informers.NewSharedInformerFactory(client, 5*time.Second) 48 | 49 | // When informer is requested, the factory instantiates it and keeps the 50 | // the reference to it in the internal map before returning. 51 | cmInformer := factory.Core().V1().ConfigMaps() 52 | cmInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ 53 | AddFunc: func(obj interface{}) { 54 | cm := obj.(*corev1.ConfigMap) 55 | fmt.Printf("Informer event: ConfigMap ADDED %s/%s\n", cm.GetNamespace(), cm.GetName()) 56 | }, 57 | UpdateFunc: func(old, new interface{}) { 58 | cm := old.(*corev1.ConfigMap) 59 | fmt.Printf("Informer event: ConfigMap UPDATED %s/%s\n", cm.GetNamespace(), cm.GetName()) 60 | }, 61 | DeleteFunc: func(obj interface{}) { 62 | cm := obj.(*corev1.ConfigMap) 63 | fmt.Printf("Informer event: ConfigMap DELETED %s/%s\n", cm.GetNamespace(), cm.GetName()) 64 | }, 65 | }) 66 | 67 | ctx, cancel := context.WithCancel(context.Background()) 68 | defer cancel() 69 | 70 | // Start the informers' machinery. 71 | // - Start() starts every Informer requested before using a goroutine per informer. 72 | // - A started Informer will fetch ALL the ConfigMaps from all the namespaces 73 | // (using a lister) and trigger `AddFunc`` for each found ConfigMap object. 74 | // Use NewSharedInformerFactoryWithOptions() to make the lister fetch only 75 | // a filtered subset of objects. 76 | // - All ConfigMaps added, updated, or deleted after the informer has been synced 77 | // will trigger the corresponding callback call (using a watch). 78 | // - Every 5*time.Second the UpdateFunc callback will be called for every 79 | // previously fetched ConfigMap (so-called resync period). 80 | factory.Start(ctx.Done()) 81 | 82 | // factory.Start() releases the execution flow without waiting for all the 83 | // internal machinery to warm up. We use factory.WaitForCacheSync() here 84 | // to poll for cmInformer.Informer().HasSynced(). Essentially, it's just a 85 | // fancy way to write a while-loop checking HasSynced() flags for all the 86 | // registered informers with 100ms delay between iterations. 87 | for informerType, ok := range factory.WaitForCacheSync(ctx.Done()) { 88 | if !ok { 89 | panic(fmt.Sprintf("Failed to sync cache for %v", informerType)) 90 | } 91 | } 92 | 93 | // Search for the existing ConfigMap object using the label selector. 94 | selector, err := labels.Parse("example==" + label) 95 | if err != nil { 96 | panic(err.Error()) 97 | } 98 | list, err := cmInformer.Lister().List(selector) 99 | if err != nil { 100 | panic(err.Error()) 101 | } 102 | if len(list) != 1 { 103 | panic("expected ConfigMap not found") 104 | } 105 | 106 | // Create another object while watching. 107 | second := createConfigMap(client) 108 | 109 | // Delete config maps created by this test. 110 | deleteConfigMap(client, first) 111 | deleteConfigMap(client, second) 112 | 113 | // Stay for a couple more seconds to observe resyncs. 114 | time.Sleep(10 * time.Second) 115 | } 116 | 117 | func createConfigMap(client kubernetes.Interface) *corev1.ConfigMap { 118 | cm := &corev1.ConfigMap{Data: map[string]string{"foo": "bar"}} 119 | cm.Namespace = namespace 120 | cm.GenerateName = "informer-typed-simple-" 121 | cm.SetLabels(map[string]string{"example": label}) 122 | 123 | cm, err := client. 124 | CoreV1(). 125 | ConfigMaps(namespace). 126 | Create( 127 | context.Background(), 128 | cm, 129 | metav1.CreateOptions{}, 130 | ) 131 | if err != nil { 132 | panic(err.Error()) 133 | } 134 | 135 | fmt.Printf("Created ConfigMap %s/%s\n", cm.GetNamespace(), cm.GetName()) 136 | return cm 137 | } 138 | 139 | func deleteConfigMap(client kubernetes.Interface, cm *corev1.ConfigMap) { 140 | err := client. 141 | CoreV1(). 142 | ConfigMaps(cm.GetNamespace()). 143 | Delete( 144 | context.Background(), 145 | cm.GetName(), 146 | metav1.DeleteOptions{}, 147 | ) 148 | if err != nil { 149 | panic(err.Error()) 150 | } 151 | 152 | fmt.Printf("Deleted ConfigMap %s/%s\n", cm.GetNamespace(), cm.GetName()) 153 | } 154 | -------------------------------------------------------------------------------- /kubeconfig-default-context/Makefile: -------------------------------------------------------------------------------- 1 | CUR_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | 3 | 4 | .PHONY: test 5 | test: go-mod-tidy 6 | go run ${CUR_DIR}/main.go 7 | 8 | .PHONY: go-mod-tidy 9 | go-mod-tidy: 10 | cd ${CUR_DIR} && go mod tidy -------------------------------------------------------------------------------- /kubeconfig-default-context/README.md: -------------------------------------------------------------------------------- 1 | # Create config from kubeconfig file using clientcmd.BuildConfigFromFlags() 2 | 3 | The example code looks simple, but the internals aren't so simple, actually. 4 | `clientcmd.BuildConfigFromFlags()` under the hood uses the `DeferredLoadingClientConfig` struct 5 | (via `NewNonInteractiveDeferredLoadingClientConfig`) that implements `ClientConfigLoader` interface. 6 | Such kind of loader is needed to defer the actual config creation util all the possible tweaks (via 7 | extra flags and/or env vars) are done. However, since the `ExplicitPath` is used, neither the deferred 8 | loading nor actual merging happens. 9 | -------------------------------------------------------------------------------- /kubeconfig-default-context/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iximiuz/client-go-examples/kubeconfig-default-context 2 | 3 | go 1.22.10 4 | 5 | require ( 6 | k8s.io/client-go v0.30.1 7 | ) 8 | -------------------------------------------------------------------------------- /kubeconfig-default-context/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | 8 | "k8s.io/client-go/discovery" 9 | "k8s.io/client-go/tools/clientcmd" 10 | ) 11 | 12 | func main() { 13 | home, err := os.UserHomeDir() 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | config, err := clientcmd.BuildConfigFromFlags("", path.Join(home, ".kube/config")) 19 | if err != nil { 20 | panic(err.Error()) 21 | } 22 | 23 | client, err := discovery.NewDiscoveryClientForConfig(config) 24 | if err != nil { 25 | panic(err.Error()) 26 | } 27 | 28 | ver, err := client.ServerVersion() 29 | if err != nil { 30 | panic(err.Error()) 31 | } 32 | 33 | fmt.Println(ver.String()) 34 | } 35 | -------------------------------------------------------------------------------- /kubeconfig-from-yaml/Makefile: -------------------------------------------------------------------------------- 1 | CUR_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | 3 | 4 | .PHONY: test 5 | test: go-mod-tidy 6 | go run ${CUR_DIR}/main.go 7 | 8 | .PHONY: go-mod-tidy 9 | go-mod-tidy: 10 | cd ${CUR_DIR} && go mod tidy -------------------------------------------------------------------------------- /kubeconfig-from-yaml/README.md: -------------------------------------------------------------------------------- 1 | # Deserialize kubeconfig YAML into kubeconfig Go struct and create rest.Config out of it 2 | 3 | The trick is helpful when the Kubeconfig content is obtained from a non-disk location (e.g., read from a Kubernetes secret). -------------------------------------------------------------------------------- /kubeconfig-from-yaml/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iximiuz/client-go-examples/kubeconfig-from-yaml 2 | 3 | go 1.22.10 4 | 5 | require ( 6 | k8s.io/client-go v0.30.1 7 | ) 8 | -------------------------------------------------------------------------------- /kubeconfig-from-yaml/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path" 8 | 9 | "k8s.io/client-go/discovery" 10 | "k8s.io/client-go/tools/clientcmd" 11 | "k8s.io/client-go/tools/clientcmd/api" 12 | ) 13 | 14 | func main() { 15 | home, err := os.UserHomeDir() 16 | if err != nil { 17 | panic(err.Error()) 18 | } 19 | 20 | kubeconfigGetter := func() (*api.Config, error) { 21 | kubeconfigYAML, err := ioutil.ReadFile(path.Join(home, ".kube/config")) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return clientcmd.Load([]byte(kubeconfigYAML)) 26 | } 27 | 28 | config, err := clientcmd.BuildConfigFromKubeconfigGetter("", kubeconfigGetter) 29 | if err != nil { 30 | panic(err.Error()) 31 | } 32 | 33 | client, err := discovery.NewDiscoveryClientForConfig(config) 34 | if err != nil { 35 | panic(err.Error()) 36 | } 37 | 38 | ver, err := client.ServerVersion() 39 | if err != nil { 40 | panic(err.Error()) 41 | } 42 | 43 | fmt.Println(ver.String()) 44 | } 45 | -------------------------------------------------------------------------------- /kubeconfig-list-contexts/Makefile: -------------------------------------------------------------------------------- 1 | CUR_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | 3 | 4 | .PHONY: test 5 | test: go-mod-tidy 6 | go run ${CUR_DIR}/main.go 7 | 8 | .PHONY: go-mod-tidy 9 | go-mod-tidy: 10 | cd ${CUR_DIR} && go mod tidy -------------------------------------------------------------------------------- /kubeconfig-list-contexts/README.md: -------------------------------------------------------------------------------- 1 | # List Contexts defined in kubeconfig file -------------------------------------------------------------------------------- /kubeconfig-list-contexts/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iximiuz/client-go-examples/kubeconfig-list-contexts 2 | 3 | go 1.22.10 4 | 5 | require ( 6 | k8s.io/client-go v0.30.1 7 | ) 8 | -------------------------------------------------------------------------------- /kubeconfig-list-contexts/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | 8 | "k8s.io/client-go/tools/clientcmd" 9 | ) 10 | 11 | func main() { 12 | home, err := os.UserHomeDir() 13 | if err != nil { 14 | panic(err) 15 | } 16 | 17 | config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( 18 | &clientcmd.ClientConfigLoadingRules{ExplicitPath: path.Join(home, ".kube/config")}, 19 | &clientcmd.ConfigOverrides{}, 20 | ).RawConfig() 21 | if err != nil { 22 | panic(err.Error()) 23 | } 24 | 25 | for name := range config.Contexts { 26 | fmt.Printf("Found context %s\n", name) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /kubeconfig-overridden-context/Makefile: -------------------------------------------------------------------------------- 1 | CUR_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | 3 | 4 | .PHONY: test 5 | test: go-mod-tidy 6 | kind create cluster --name cluster2 7 | go run ${CUR_DIR}/main.go kind-cluster2 8 | 9 | .PHONY: go-mod-tidy 10 | go-mod-tidy: 11 | cd ${CUR_DIR} && go mod tidy -------------------------------------------------------------------------------- /kubeconfig-overridden-context/README.md: -------------------------------------------------------------------------------- 1 | # Create config from a specific kubeconfig's context -------------------------------------------------------------------------------- /kubeconfig-overridden-context/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iximiuz/client-go-examples/kubeconfig-overriden-context 2 | 3 | go 1.22.10 4 | 5 | require ( 6 | k8s.io/client-go v0.30.1 7 | ) 8 | -------------------------------------------------------------------------------- /kubeconfig-overridden-context/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | 8 | "k8s.io/client-go/discovery" 9 | "k8s.io/client-go/tools/clientcmd" 10 | ) 11 | 12 | func main() { 13 | home, err := os.UserHomeDir() 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( 19 | &clientcmd.ClientConfigLoadingRules{ExplicitPath: path.Join(home, ".kube/config")}, 20 | &clientcmd.ConfigOverrides{CurrentContext: os.Args[1]}, 21 | ).ClientConfig() 22 | if err != nil { 23 | panic(err.Error()) 24 | } 25 | 26 | client, err := discovery.NewDiscoveryClientForConfig(config) 27 | if err != nil { 28 | panic(err.Error()) 29 | } 30 | 31 | ver, err := client.ServerVersion() 32 | if err != nil { 33 | panic(err.Error()) 34 | } 35 | 36 | fmt.Println(ver.String()) 37 | } 38 | -------------------------------------------------------------------------------- /label-selectors/Makefile: -------------------------------------------------------------------------------- 1 | CUR_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | 3 | 4 | .PHONY: test 5 | test: go-mod-tidy 6 | go run ${CUR_DIR}/main.go 7 | 8 | .PHONY: go-mod-tidy 9 | go-mod-tidy: 10 | cd ${CUR_DIR} && go mod tidy -------------------------------------------------------------------------------- /label-selectors/README.md: -------------------------------------------------------------------------------- 1 | # How to construct labels and label selectors -------------------------------------------------------------------------------- /label-selectors/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iximiuz/client-go-examples/label-selectors 2 | 3 | go 1.22.10 4 | 5 | require ( 6 | k8s.io/apimachinery v0.30.1 7 | ) 8 | -------------------------------------------------------------------------------- /label-selectors/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "k8s.io/apimachinery/pkg/labels" 7 | "k8s.io/apimachinery/pkg/selection" 8 | ) 9 | 10 | func main() { 11 | lbls := labels.Set{"foo": "bar", "baz": "qux"} 12 | 13 | sel := labels.NewSelector() 14 | req, err := labels.NewRequirement("foo", selection.Equals, []string{"bar"}) 15 | if err != nil { 16 | panic(err.Error()) 17 | } 18 | sel = sel.Add(*req) 19 | if sel.Matches(lbls) { 20 | fmt.Printf("Selector %v matched label set %v\n", sel, lbls) 21 | } else { 22 | panic("Selector should have matched labels") 23 | } 24 | 25 | // Selector from string expression. 26 | sel, err = labels.Parse("foo==bar") 27 | if err != nil { 28 | panic(err.Error()) 29 | } 30 | if sel.Matches(lbls) { 31 | fmt.Printf("Selector %v matched label set %v\n", sel, lbls) 32 | } else { 33 | panic("Selector should have matched labels") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /list-typed-simple/Makefile: -------------------------------------------------------------------------------- 1 | CUR_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | 3 | 4 | .PHONY: test 5 | test: go-mod-tidy 6 | go run ${CUR_DIR}/main.go 7 | 8 | .PHONY: go-mod-tidy 9 | go-mod-tidy: 10 | cd ${CUR_DIR} && go mod tidy -------------------------------------------------------------------------------- /list-typed-simple/README.md: -------------------------------------------------------------------------------- 1 | # Cache-less listing of Kubernetes objects using typed client -------------------------------------------------------------------------------- /list-typed-simple/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iximiuz/client-go-examples/list-typed-simple 2 | 3 | go 1.22.10 4 | 5 | require ( 6 | k8s.io/api v0.30.1 7 | k8s.io/apimachinery v0.30.1 8 | k8s.io/client-go v0.30.1 9 | ) 10 | -------------------------------------------------------------------------------- /list-typed-simple/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path" 8 | 9 | corev1 "k8s.io/api/core/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/util/rand" 12 | "k8s.io/client-go/kubernetes" 13 | "k8s.io/client-go/tools/clientcmd" 14 | ) 15 | 16 | func main() { 17 | home, err := os.UserHomeDir() 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | config, err := clientcmd.BuildConfigFromFlags("", path.Join(home, ".kube/config")) 23 | if err != nil { 24 | panic(err.Error()) 25 | } 26 | 27 | client, err := kubernetes.NewForConfig(config) 28 | if err != nil { 29 | panic(err.Error()) 30 | } 31 | 32 | namespace := "default" 33 | label := "list-typed-simple-" + rand.String(6) 34 | 35 | desired := corev1.ConfigMap{Data: map[string]string{"foo": "bar"}} 36 | desired.Namespace = namespace 37 | desired.GenerateName = "list-typed-simple-" 38 | desired.SetLabels(map[string]string{"example": label}) 39 | 40 | // Create a bunch of objects first. 41 | for i := 0; i < 10; i++ { 42 | created, err := client. 43 | CoreV1(). 44 | ConfigMaps(namespace). 45 | Create( 46 | context.Background(), 47 | &desired, 48 | metav1.CreateOptions{}, 49 | ) 50 | if err != nil { 51 | panic(err.Error()) 52 | } 53 | fmt.Printf("Created ConfigMap %s/%s\n", namespace, created.GetName()) 54 | } 55 | 56 | // List - filter by the `example` label. 57 | list, err := client. 58 | CoreV1(). 59 | ConfigMaps(namespace). 60 | List( 61 | context.Background(), 62 | metav1.ListOptions{ 63 | LabelSelector: "example==" + label, 64 | }, 65 | ) 66 | if err != nil { 67 | panic(err.Error()) 68 | } 69 | 70 | fmt.Printf("Found %d ConfigMap objects\n", len(list.Items)) 71 | } 72 | -------------------------------------------------------------------------------- /patch-add-ephemeral-container/Makefile: -------------------------------------------------------------------------------- 1 | CUR_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | 3 | 4 | .PHONY: test 5 | test: go-mod-tidy 6 | go run ${CUR_DIR}/main.go 7 | 8 | .PHONY: go-mod-tidy 9 | go-mod-tidy: 10 | cd ${CUR_DIR} && go mod tidy -------------------------------------------------------------------------------- /patch-add-ephemeral-container/README.md: -------------------------------------------------------------------------------- 1 | # PATCH Example: Add Ephemeral Container to Pod -------------------------------------------------------------------------------- /patch-add-ephemeral-container/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iximiuz/client-go-examples/patch-add-ephemeral-container 2 | 3 | go 1.22.10 4 | 5 | require ( 6 | k8s.io/api v0.30.1 7 | k8s.io/apimachinery v0.30.1 8 | k8s.io/client-go v0.30.1 9 | ) 10 | -------------------------------------------------------------------------------- /patch-add-ephemeral-container/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "path" 9 | 10 | corev1 "k8s.io/api/core/v1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/apimachinery/pkg/types" 13 | "k8s.io/apimachinery/pkg/util/strategicpatch" 14 | "k8s.io/client-go/kubernetes" 15 | "k8s.io/client-go/tools/clientcmd" 16 | ) 17 | 18 | var ( 19 | ctx = context.Background() 20 | namespace = "default" 21 | ) 22 | 23 | func main() { 24 | // 0. Initialize the Kubernetes client. 25 | home, err := os.UserHomeDir() 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | config, err := clientcmd.BuildConfigFromFlags("", path.Join(home, ".kube/config")) 31 | if err != nil { 32 | panic(err.Error()) 33 | } 34 | 35 | client, err := kubernetes.NewForConfig(config) 36 | if err != nil { 37 | panic(err.Error()) 38 | } 39 | 40 | // 1. Create a pod. 41 | pod, err := client.CoreV1(). 42 | Pods(namespace). 43 | Create(ctx, &corev1.Pod{ 44 | ObjectMeta: metav1.ObjectMeta{ 45 | Name: "patch-add-ephemeral-container", 46 | Namespace: namespace, 47 | }, 48 | Spec: corev1.PodSpec{ 49 | Containers: []corev1.Container{ 50 | { 51 | Name: "app", 52 | Image: "alpine:3", 53 | Command: []string{"/bin/sh", "-c", "sleep 999"}, 54 | }, 55 | }, 56 | }, 57 | }, metav1.CreateOptions{}) 58 | if err != nil { 59 | panic(err.Error()) 60 | } 61 | defer func() { 62 | _ = client.CoreV1().Pods(pod.Namespace).Delete(ctx, pod.Name, metav1.DeleteOptions{}) 63 | }() 64 | 65 | // 2. Add an ephemeral container to the pod spec. 66 | podWithEphemeralContainer := withDebugContainer(pod) 67 | 68 | // 3. Prepare the patch. 69 | podJSON, err := json.Marshal(pod) 70 | if err != nil { 71 | panic(err.Error()) 72 | } 73 | 74 | podWithEphemeralContainerJSON, err := json.Marshal(podWithEphemeralContainer) 75 | if err != nil { 76 | panic(err.Error()) 77 | } 78 | 79 | patch, err := strategicpatch.CreateTwoWayMergePatch(podJSON, podWithEphemeralContainerJSON, pod) 80 | if err != nil { 81 | panic(err.Error()) 82 | } 83 | 84 | // 4. Apply the patch. 85 | pod, err = client.CoreV1(). 86 | Pods(pod.Namespace). 87 | Patch( 88 | ctx, 89 | pod.Name, 90 | types.StrategicMergePatchType, 91 | patch, 92 | metav1.PatchOptions{}, 93 | "ephemeralcontainers", 94 | ) 95 | if err != nil { 96 | panic(err.Error()) 97 | } 98 | 99 | fmt.Printf("Pod has %d ephemeral containers.\n", len(pod.Spec.EphemeralContainers)) 100 | } 101 | 102 | func withDebugContainer(pod *corev1.Pod) *corev1.Pod { 103 | ec := &corev1.EphemeralContainer{ 104 | EphemeralContainerCommon: corev1.EphemeralContainerCommon{ 105 | Name: "debugger-123", 106 | Image: "busybox:musl", 107 | ImagePullPolicy: corev1.PullIfNotPresent, 108 | Command: []string{"sh"}, 109 | Stdin: true, 110 | TTY: true, 111 | TerminationMessagePolicy: corev1.TerminationMessageReadFile, 112 | }, 113 | TargetContainerName: "app", 114 | } 115 | 116 | copied := pod.DeepCopy() 117 | copied.Spec.EphemeralContainers = append(copied.Spec.EphemeralContainers, *ec) 118 | 119 | return copied 120 | } 121 | -------------------------------------------------------------------------------- /retry-on-conflict/Makefile: -------------------------------------------------------------------------------- 1 | CUR_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | 3 | 4 | .PHONY: test 5 | test: go-mod-tidy 6 | go run ${CUR_DIR}/main.go 7 | 8 | .PHONY: go-mod-tidy 9 | go-mod-tidy: 10 | cd ${CUR_DIR} && go mod tidy -------------------------------------------------------------------------------- /retry-on-conflict/README.md: -------------------------------------------------------------------------------- 1 | # Retry on Conflict 2 | 3 | `retry.RetryOnConflict(backoff wait.Backoff, fn func() error) error` is a 4 | useful function when creating custom controllers or operators. 5 | `RetryOnConflict` can help reduce the number of transient errors within 6 | your program. For more in depth information [see the official docs][]. 7 | 8 | ## Usage 9 | 10 | Typical usage for `RetryOnConflict` occurs when there's a possibility 11 | of many clients interacting with a resource at the same time or multiple controllers 12 | are interacting with the same resource, or a mixture of both. 13 | 14 | A typical implementation would be: 15 | 16 | ```golang 17 | err := retry.RetryOnConflict(retry.DefaultRetry, func() error { 18 | // Always fetch the new version of the resource 19 | pod, err := c.Pods("mynamespace").Get(name, metav1.GetOptions{}) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | // ************* 25 | // Make some form of long running change, hitting external apis, 26 | // spinning up and validating external resources, etc 27 | // ************* 28 | 29 | // Try to update 30 | _, err = c.Pods("mynamespace").UpdateStatus(pod) 31 | // You have to return err itself here (not wrapped inside another error) 32 | // so that RetryOnConflict can identify it correctly. 33 | return err 34 | }) 35 | if err != nil { 36 | return err 37 | } 38 | ``` 39 | 40 | ## This Example 41 | 42 | This example is intended to fail once. The following is the expected 43 | outcome: 44 | 45 | ```log 46 | $ go run main.go 47 | Operation cannot be fulfilled on configmaps "foobar": the object has been modified; please apply your changes to the latest version and try again 48 | Successfully updated ConfigMap 49 | ``` 50 | 51 | [see the official docs]: https://pkg.go.dev/k8s.io/client-go/util/retry#RetryOnConflict 52 | -------------------------------------------------------------------------------- /retry-on-conflict/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iximiuz/client-go-examples/retry-on-conflict 2 | 3 | go 1.22.10 4 | 5 | require ( 6 | k8s.io/api v0.30.1 7 | k8s.io/apimachinery v0.30.1 8 | k8s.io/client-go v0.30.1 9 | ) 10 | -------------------------------------------------------------------------------- /retry-on-conflict/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "os" 8 | "path" 9 | 10 | corev1 "k8s.io/api/core/v1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/client-go/kubernetes" 13 | "k8s.io/client-go/tools/clientcmd" 14 | "k8s.io/client-go/util/retry" 15 | ) 16 | 17 | var ( 18 | namespace = "default" 19 | name = "foobar" 20 | ) 21 | 22 | func main() { 23 | home, err := os.UserHomeDir() 24 | if err != nil { 25 | panic(err) 26 | } 27 | 28 | cfg, err := clientcmd.BuildConfigFromFlags("", path.Join(home, ".kube/config")) 29 | if err != nil { 30 | panic(err.Error()) 31 | } 32 | 33 | client := kubernetes.NewForConfigOrDie(cfg) 34 | desired := corev1.ConfigMap{ 35 | ObjectMeta: metav1.ObjectMeta{ 36 | Name: name, 37 | Namespace: namespace, 38 | }, 39 | Data: map[string]string{"foo": "bar"}, 40 | } 41 | 42 | _, err = client. 43 | CoreV1(). 44 | ConfigMaps(namespace). 45 | Create( 46 | context.Background(), 47 | &desired, 48 | metav1.CreateOptions{}, 49 | ) 50 | if err != nil { 51 | panic(err.Error()) 52 | } 53 | defer deleteConfigMap(client, name, namespace) 54 | 55 | firstTry := true 56 | err = retry.RetryOnConflict(retry.DefaultRetry, func() error { 57 | // always fetch the new version from the api server 58 | c, err := getConfigMap(client, name, namespace) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | // simulate an external update 64 | // this code will not exist in a typical implementation 65 | // this is just for demonstration 66 | if firstTry { 67 | firstTry = false 68 | simulateExternalUpdate(client, name, namespace) 69 | } 70 | 71 | // generate random int so we're always different 72 | c.Data = map[string]string{ 73 | "changed": fmt.Sprint(rand.Intn(100)), 74 | } 75 | 76 | _, err = client. 77 | CoreV1(). 78 | ConfigMaps(namespace). 79 | Update( 80 | context.Background(), 81 | c, 82 | metav1.UpdateOptions{}, 83 | ) 84 | if err != nil { 85 | fmt.Println(err) 86 | } 87 | return err 88 | }) 89 | if err != nil { 90 | // ensure no other error type occurred 91 | panic(err) 92 | } 93 | fmt.Println("Successfully updated ConfigMap") 94 | } 95 | 96 | func simulateExternalUpdate(k *kubernetes.Clientset, name, ns string) { 97 | cm, err := getConfigMap(k, name, ns) 98 | if err != nil { 99 | panic(err) 100 | } 101 | cm.Data = map[string]string{ 102 | "external": "update", 103 | } 104 | _, err = k.CoreV1(). 105 | ConfigMaps(ns). 106 | Update( 107 | context.Background(), 108 | cm, 109 | metav1.UpdateOptions{}, 110 | ) 111 | if err != nil { 112 | panic(err) 113 | } 114 | } 115 | 116 | func getConfigMap(k *kubernetes.Clientset, name, ns string) (*corev1.ConfigMap, error) { 117 | return k. 118 | CoreV1(). 119 | ConfigMaps(ns). 120 | Get( 121 | context.Background(), 122 | name, 123 | metav1.GetOptions{}, 124 | ) 125 | } 126 | 127 | func deleteConfigMap(c *kubernetes.Clientset, name, ns string) { 128 | c.CoreV1().ConfigMaps(ns).Delete(context.Background(), name, metav1.DeleteOptions{}) 129 | } 130 | -------------------------------------------------------------------------------- /serialize-typed-json/Makefile: -------------------------------------------------------------------------------- 1 | CUR_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | 3 | 4 | .PHONY: test 5 | test: go-mod-tidy 6 | go run ${CUR_DIR}/main.go 7 | 8 | .PHONY: go-mod-tidy 9 | go-mod-tidy: 10 | cd ${CUR_DIR} && go mod tidy -------------------------------------------------------------------------------- /serialize-typed-json/README.md: -------------------------------------------------------------------------------- 1 | # How to serialize typed Kubernetes objects into JSON and vice versa -------------------------------------------------------------------------------- /serialize-typed-json/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iximiuz/client-go-examples/serialize-typed-json 2 | 3 | go 1.22.10 4 | 5 | require ( 6 | k8s.io/api v0.30.1 7 | k8s.io/apimachinery v0.30.1 8 | k8s.io/client-go v0.30.1 9 | ) 10 | -------------------------------------------------------------------------------- /serialize-typed-json/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | corev1 "k8s.io/api/core/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | jsonserializer "k8s.io/apimachinery/pkg/runtime/serializer/json" 11 | "k8s.io/client-go/kubernetes/scheme" 12 | ) 13 | 14 | func main() { 15 | obj := corev1.ConfigMap{ 16 | TypeMeta: metav1.TypeMeta{ 17 | Kind: "ConfigMap", 18 | APIVersion: "v1", 19 | }, 20 | Data: map[string]string{"foo": "bar"}, 21 | } 22 | obj.Namespace = "default" 23 | obj.Name = "my-configmap" 24 | 25 | // Typed -> JSON (Option I) 26 | // - Serializer = Decoder + Encoder. Since we need only Encoder functionality 27 | // in this example, we can pass nil's instead of MetaFactory, Creater, and 28 | // Typer arguments as they are used only by the Decoder. 29 | encoder := jsonserializer.NewSerializerWithOptions( 30 | nil, // jsonserializer.MetaFactory 31 | nil, // runtime.ObjectCreater 32 | nil, // runtime.ObjectTyper 33 | jsonserializer.SerializerOptions{ 34 | Yaml: false, 35 | Pretty: false, 36 | Strict: false, 37 | }, 38 | ) 39 | 40 | // Runtime.Encode() is just a helper function to invoke Encoder.Encode() 41 | encoded, err := runtime.Encode(encoder, &obj) 42 | if err != nil { 43 | panic(err.Error()) 44 | } 45 | fmt.Println("Serialized (option I)", string(encoded)) 46 | 47 | // Typed -> JSON (Option II) 48 | // Actually, the implementation of Encoder.Encode() in the case of JSON 49 | // boils down to calling the stdlib encoding/json.Marshal() with optional 50 | // pretty-printing and converting JSON to YAML. 51 | // See https://github.com/kubernetes/apimachinery/blob/73cb564852596cc976f3ead9e0f4678875af0cbf/pkg/runtime/serializer/json/json.go#L210-L234 52 | encoded2, err := json.Marshal(obj) 53 | if err != nil { 54 | panic(err.Error()) 55 | } 56 | fmt.Println("Serialized (option II)", string(encoded2)) 57 | 58 | // JSON -> Typed 59 | // - Serializer = Decoder + Encoder. 60 | // - jsonserializer.MetaFactory is a simple partial JSON unmarshaller that 61 | // looks for APIGroup/Version and Kind attributes in the supplied 62 | // piece of JSON and parses them into a schema.GroupVersionKind{} object. 63 | // - runtime.ObjectCreater is used to create an empty typed runtime.Object 64 | // (e.g., Deployment, Pod, ConfigMap, etc.) for the provided APIGroup/Version and Kind. 65 | // - runtime.ObjectTyper is rather optional - Decoder accepts an optional 66 | // `into runtime.Object` argument, and ObjectTyper is used to make sure 67 | // the MetaFactory's GroupVersionKind matches the one from the `into` argument. 68 | decoder := jsonserializer.NewSerializerWithOptions( 69 | jsonserializer.DefaultMetaFactory, // jsonserializer.MetaFactory 70 | scheme.Scheme, // runtime.Scheme implements runtime.ObjectCreater 71 | scheme.Scheme, // runtime.Scheme implements runtime.ObjectTyper 72 | jsonserializer.SerializerOptions{ 73 | Yaml: false, 74 | Pretty: false, 75 | Strict: false, 76 | }, 77 | ) 78 | 79 | // The actual decoding is much like stdlib encoding/json.Unmarshal but with some 80 | // minor tweaks - see https://github.com/kubernetes-sigs/json for more. 81 | decoded, err := runtime.Decode(decoder, encoded) 82 | if err != nil { 83 | panic(err.Error()) 84 | } 85 | fmt.Printf("Deserialized %#v\n", decoded) 86 | } 87 | -------------------------------------------------------------------------------- /serialize-typed-yaml/Makefile: -------------------------------------------------------------------------------- 1 | CUR_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | 3 | 4 | .PHONY: test 5 | test: go-mod-tidy 6 | go run ${CUR_DIR}/main.go 7 | 8 | .PHONY: go-mod-tidy 9 | go-mod-tidy: 10 | cd ${CUR_DIR} && go mod tidy -------------------------------------------------------------------------------- /serialize-typed-yaml/README.md: -------------------------------------------------------------------------------- 1 | # How to serialize typed Kubernetes objects into YAML and vice versa -------------------------------------------------------------------------------- /serialize-typed-yaml/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iximiuz/client-go-examples/serialize-typed-yaml 2 | 3 | go 1.22.10 4 | 5 | require ( 6 | k8s.io/api v0.30.1 7 | k8s.io/apimachinery v0.30.1 8 | k8s.io/client-go v0.30.1 9 | ) 10 | -------------------------------------------------------------------------------- /serialize-typed-yaml/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | corev1 "k8s.io/api/core/v1" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | jsonserializer "k8s.io/apimachinery/pkg/runtime/serializer/json" 10 | "k8s.io/client-go/kubernetes/scheme" 11 | ) 12 | 13 | // See serialize-typed-json example for more detailed explanation 14 | // of encoding/decoding machinery. 15 | 16 | func main() { 17 | obj := corev1.ConfigMap{ 18 | TypeMeta: metav1.TypeMeta{ 19 | Kind: "ConfigMap", 20 | APIVersion: "v1", 21 | }, 22 | Data: map[string]string{"foo": "bar"}, 23 | } 24 | obj.Namespace = "default" 25 | obj.Name = "my-configmap" 26 | 27 | // Serializer = Decoder + Encoder. 28 | serializer := jsonserializer.NewSerializerWithOptions( 29 | jsonserializer.DefaultMetaFactory, // jsonserializer.MetaFactory 30 | scheme.Scheme, // runtime.Scheme implements runtime.ObjectCreater 31 | scheme.Scheme, // runtime.Scheme implements runtime.ObjectTyper 32 | jsonserializer.SerializerOptions{ 33 | Yaml: true, 34 | Pretty: false, 35 | Strict: false, 36 | }, 37 | ) 38 | 39 | // Typed -> YAML 40 | // Runtime.Encode() is just a helper function to invoke Encoder.Encode() 41 | yaml, err := runtime.Encode(serializer, &obj) 42 | if err != nil { 43 | panic(err.Error()) 44 | } 45 | fmt.Printf("Serialized:\n%s", string(yaml)) 46 | 47 | // YAML -> Typed (through JSON, actually) 48 | decoded, err := runtime.Decode(serializer, yaml) 49 | if err != nil { 50 | panic(err.Error()) 51 | } 52 | fmt.Printf("Deserialized: %#v\n", decoded) 53 | } 54 | -------------------------------------------------------------------------------- /serialize-unstructured-json/Makefile: -------------------------------------------------------------------------------- 1 | CUR_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | 3 | 4 | .PHONY: test 5 | test: go-mod-tidy 6 | go run ${CUR_DIR}/main.go 7 | 8 | .PHONY: go-mod-tidy 9 | go-mod-tidy: 10 | cd ${CUR_DIR} && go mod tidy -------------------------------------------------------------------------------- /serialize-unstructured-json/README.md: -------------------------------------------------------------------------------- 1 | # How to serialize unstructured objects into JSON and vice versa 2 | 3 | This mini-program demonstrates usage of: 4 | 5 | - `k8s.io/apimachinery/pkg/runtime.Encoder` 6 | - `k8s.io/apimachinery/pkg/runtime.Decoder` 7 | - `k8s.io/apimachinery/pkg/runtime.Serializer` 8 | - `k8s.io/apimachinery/pkg/runtime.Codec` -------------------------------------------------------------------------------- /serialize-unstructured-json/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iximiuz/client-go-examples/serialize-unstructured-json 2 | 3 | go 1.22.10 4 | 5 | require ( 6 | k8s.io/apimachinery v0.30.1 7 | ) 8 | -------------------------------------------------------------------------------- /serialize-unstructured-json/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | ) 10 | 11 | func main() { 12 | uConfigMap := unstructured.Unstructured{ 13 | Object: map[string]interface{}{ 14 | "apiVersion": "v1", 15 | "kind": "ConfigMap", 16 | "metadata": map[string]interface{}{ 17 | "creationTimestamp": nil, 18 | "namespace": "default", 19 | "name": "my-configmap", 20 | }, 21 | "data": map[string]interface{}{ 22 | "foo": "bar", 23 | }, 24 | }, 25 | } 26 | 27 | // Unstructured -> JSON (Option I) 28 | // - Despite the name, `UnstructuredJSONScheme` is not a scheme but a codec 29 | // - runtime.Encode() is just a helper function to invoke UnstructuredJSONScheme.Encode() 30 | // - UnstructuredJSONScheme.Encode() is needed because the unstructured instance can be 31 | // either a single object, a list, or an unknown runtime object, so some amount of 32 | // preprocessing is required before passing the data to json.Marshal() 33 | // - Usage example: dynamic client (client-go/dynamic.Interface) 34 | bytes, err := runtime.Encode(unstructured.UnstructuredJSONScheme, &uConfigMap) 35 | fmt.Println("Serialized (option I)", string(bytes)) 36 | 37 | // Unstructured -> JSON (Option II) 38 | // - This is just a handy shortcut for the above code. 39 | bytes, err = uConfigMap.MarshalJSON() 40 | if err != nil { 41 | panic(err.Error()) 42 | } 43 | fmt.Println("Serialized (option II)", string(bytes)) 44 | 45 | // JSON -> Unstructured (Option I) 46 | // - Usage example: dynamic client (client-go/dynamic.Interface) 47 | obj1, err := runtime.Decode(unstructured.UnstructuredJSONScheme, bytes) 48 | if err != nil { 49 | panic(err.Error()) 50 | } 51 | 52 | // JSON -> Unstructured (Option II) 53 | // - This is just a handy shortcut for the above code. 54 | obj2 := &unstructured.Unstructured{} 55 | err = obj2.UnmarshalJSON(bytes) 56 | if err != nil { 57 | panic(err.Error()) 58 | } 59 | if !reflect.DeepEqual(obj1, obj2) { 60 | panic("Unexpected configmap data") 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /serialize-unstructured-yaml/Makefile: -------------------------------------------------------------------------------- 1 | CUR_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | 3 | 4 | .PHONY: test 5 | test: go-mod-tidy 6 | go run ${CUR_DIR}/main.go 7 | 8 | .PHONY: go-mod-tidy 9 | go-mod-tidy: 10 | cd ${CUR_DIR} && go mod tidy -------------------------------------------------------------------------------- /serialize-unstructured-yaml/README.md: -------------------------------------------------------------------------------- 1 | # How to deserialize YAML into unstructured objects 2 | 3 | This mini-program demonstrates usage of: 4 | 5 | - `k8s.io/apimachinery/pkg/util/yaml` 6 | - `k8s.io/apimachinery/pkg/runtime.Serializer` -------------------------------------------------------------------------------- /serialize-unstructured-yaml/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iximiuz/client-go-examples/serialize-unstructured-yaml 2 | 3 | go 1.22.10 4 | 5 | require ( 6 | k8s.io/apimachinery v0.30.1 7 | ) 8 | -------------------------------------------------------------------------------- /serialize-unstructured-yaml/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | "k8s.io/apimachinery/pkg/util/yaml" 7 | ) 8 | 9 | func main() { 10 | yConfigMap := `--- 11 | apiVersion: v1 12 | data: 13 | foo: bar 14 | kind: ConfigMap 15 | metadata: 16 | creationTimestamp: 17 | name: my-configmap 18 | namespace: default 19 | ` 20 | 21 | // YAML -> Unstructured (through JSON) 22 | jConfigMap, err := yaml.ToJSON([]byte(yConfigMap)) 23 | if err != nil { 24 | panic(err.Error()) 25 | } 26 | 27 | object, err := runtime.Decode(unstructured.UnstructuredJSONScheme, jConfigMap) 28 | if err != nil { 29 | panic(err.Error()) 30 | } 31 | 32 | uConfigMap, ok := object.(*unstructured.Unstructured) 33 | if !ok { 34 | panic("unstructured.Unstructured expected") 35 | } 36 | 37 | if uConfigMap.GetName() != "my-configmap" { 38 | panic("Unexpected configmap data") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /watch-typed-simple/Makefile: -------------------------------------------------------------------------------- 1 | CUR_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | 3 | 4 | .PHONY: test 5 | test: go-mod-tidy 6 | go run ${CUR_DIR}/main.go 7 | 8 | .PHONY: go-mod-tidy 9 | go-mod-tidy: 10 | cd ${CUR_DIR} && go mod tidy -------------------------------------------------------------------------------- /watch-typed-simple/README.md: -------------------------------------------------------------------------------- 1 | # Cache-less watching of Kubernetes objects using typed client -------------------------------------------------------------------------------- /watch-typed-simple/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iximiuz/client-go-examples/watch-typed-simple 2 | 3 | go 1.22.10 4 | 5 | require ( 6 | k8s.io/api v0.30.1 7 | k8s.io/apimachinery v0.30.1 8 | k8s.io/client-go v0.30.1 9 | ) 10 | -------------------------------------------------------------------------------- /watch-typed-simple/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path" 8 | "time" 9 | 10 | corev1 "k8s.io/api/core/v1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/apimachinery/pkg/util/rand" 13 | "k8s.io/client-go/kubernetes" 14 | "k8s.io/client-go/tools/clientcmd" 15 | ) 16 | 17 | var ( 18 | namespace = "default" 19 | label = "watch-typed-simple-" + rand.String(6) 20 | ) 21 | 22 | func main() { 23 | home, err := os.UserHomeDir() 24 | if err != nil { 25 | panic(err) 26 | } 27 | 28 | config, err := clientcmd.BuildConfigFromFlags("", path.Join(home, ".kube/config")) 29 | if err != nil { 30 | panic(err.Error()) 31 | } 32 | 33 | client, err := kubernetes.NewForConfig(config) 34 | if err != nil { 35 | panic(err.Error()) 36 | } 37 | 38 | // Create one object before starting to watch. 39 | first := createConfigMap(client) 40 | 41 | // Start watching. Expected events: 42 | // - ADDED the first config map (even though it was done before starting the watch) 43 | // - ADDED the second config map 44 | // - x2 DELETED 45 | watch, err := client. 46 | CoreV1(). 47 | ConfigMaps(namespace). 48 | Watch( 49 | context.Background(), 50 | metav1.ListOptions{ 51 | LabelSelector: "example==" + label, 52 | }, 53 | ) 54 | if err != nil { 55 | panic(err.Error()) 56 | } 57 | go func() { 58 | for event := range watch.ResultChan() { 59 | fmt.Printf( 60 | "Watch Event: %s %s\n", 61 | event.Type, event.Object.GetObjectKind().GroupVersionKind().Kind, 62 | ) 63 | } 64 | }() 65 | 66 | // Create another object while watching. 67 | second := createConfigMap(client) 68 | 69 | deleteConfigMap(client, first) 70 | deleteConfigMap(client, second) 71 | 72 | time.Sleep(2 * time.Second) 73 | watch.Stop() 74 | time.Sleep(1 * time.Second) 75 | } 76 | 77 | func createConfigMap(client kubernetes.Interface) *corev1.ConfigMap { 78 | cm := &corev1.ConfigMap{Data: map[string]string{"foo": "bar"}} 79 | cm.Namespace = namespace 80 | cm.GenerateName = "watch-typed-simple-" 81 | cm.SetLabels(map[string]string{"example": label}) 82 | 83 | cm, err := client. 84 | CoreV1(). 85 | ConfigMaps(namespace). 86 | Create( 87 | context.Background(), 88 | cm, 89 | metav1.CreateOptions{}, 90 | ) 91 | if err != nil { 92 | panic(err.Error()) 93 | } 94 | 95 | fmt.Printf("Created ConfigMap %s/%s\n", cm.GetNamespace(), cm.GetName()) 96 | return cm 97 | } 98 | 99 | func deleteConfigMap(client kubernetes.Interface, cm *corev1.ConfigMap) { 100 | err := client. 101 | CoreV1(). 102 | ConfigMaps(cm.GetNamespace()). 103 | Delete( 104 | context.Background(), 105 | cm.GetName(), 106 | metav1.DeleteOptions{}, 107 | ) 108 | if err != nil { 109 | panic(err.Error()) 110 | } 111 | 112 | fmt.Printf("Deleted ConfigMap %s/%s\n", cm.GetNamespace(), cm.GetName()) 113 | } 114 | -------------------------------------------------------------------------------- /workqueue/Makefile: -------------------------------------------------------------------------------- 1 | CUR_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | 3 | 4 | .PHONY: test 5 | test: go-mod-tidy 6 | go run ${CUR_DIR}/main.go 7 | 8 | .PHONY: go-mod-tidy 9 | go-mod-tidy: 10 | cd ${CUR_DIR} && go mod tidy -------------------------------------------------------------------------------- /workqueue/README.md: -------------------------------------------------------------------------------- 1 | # workqueue example - controllers' fundamentals 2 | 3 | Based on client-go `examples/workqueue`. 4 | The example shows how to implement a primitive controller watching ADD/UPDATE/DELETE events for a particular kind of object. 5 | The events are queued to allow safe parallel processing. 6 | -------------------------------------------------------------------------------- /workqueue/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iximiuz/client-go-examples/workqueue 2 | 3 | go 1.22.10 4 | 5 | require ( 6 | k8s.io/apimachinery v0.30.1 7 | k8s.io/client-go v0.30.1 8 | ) 9 | -------------------------------------------------------------------------------- /workqueue/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path" 8 | "time" 9 | 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 12 | "k8s.io/apimachinery/pkg/runtime/schema" 13 | "k8s.io/client-go/dynamic" 14 | "k8s.io/client-go/dynamic/dynamicinformer" 15 | "k8s.io/client-go/tools/cache" 16 | "k8s.io/client-go/tools/clientcmd" 17 | "k8s.io/client-go/util/workqueue" 18 | ) 19 | 20 | var ( 21 | namespace = "default" 22 | ConfigMapResource = schema.GroupVersionResource{ 23 | Group: "", 24 | Version: "v1", 25 | Resource: "configmaps", 26 | } 27 | ) 28 | 29 | func main() { 30 | client := createClientOrDie() 31 | 32 | // The work queue has the following properties: 33 | // - Fair: items processed in the order in which they are added. 34 | // - Stingy: a single item will not be processed multiple times concurrently, 35 | // and if an item is added multiple times before it can be processed, it 36 | // will only be processed once. 37 | // - Multitenant: Multiple consumers and producers. In particular, it is allowed for an 38 | // item to be reenqueued while it is being processed. 39 | queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) 40 | defer queue.ShutDown() 41 | 42 | // The queue is typically populated by one or more informers watching events 43 | // on Kubernetes resources. An "idiomatic" way to get an informer is via 44 | // a SharedInformerFactory. 45 | // - A factory is essentially a struct keeping a map (type -> informer). 46 | // - 5*time.Second is a default resync period (for all informers). 47 | // - namespace makes the informers watch only the specified namespace. 48 | // - an extra func allows to tweak other listing options like label- or field- selectors. 49 | factory := dynamicinformer.NewFilteredDynamicSharedInformerFactory( 50 | client, 5*time.Second, namespace, func(*metav1.ListOptions) {}, 51 | ) 52 | dynamicInformer := factory.ForResource(ConfigMapResource) 53 | 54 | // Informer watches a resource (ConfigMap in this particular example) 55 | // and simply pushes object keys to the queue. 56 | dynamicInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ 57 | AddFunc: func(obj interface{}) { 58 | // key is a string / (or just for cluster-wide objects) 59 | key, err := cache.MetaNamespaceKeyFunc(obj) 60 | if err == nil { 61 | fmt.Printf("New event: ADD %s\n", key) 62 | queue.Add(key) 63 | } 64 | }, 65 | UpdateFunc: func(old, new interface{}) { 66 | key, err := cache.MetaNamespaceKeyFunc(new) 67 | if err == nil { 68 | fmt.Printf("New event: UPDATE %s\n", key) 69 | queue.Add(key) 70 | } 71 | }, 72 | DeleteFunc: func(obj interface{}) { 73 | // much like cache.MetaNamespaceKeyFunc + some extra check. 74 | key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) 75 | if err == nil { 76 | fmt.Printf("New event: DELETE %s\n", key) 77 | queue.Add(key) 78 | } 79 | }, 80 | }) 81 | 82 | ctx, cancel := context.WithCancel(context.Background()) 83 | defer cancel() 84 | 85 | // Start the informers' machinery. 86 | factory.Start(ctx.Done()) 87 | 88 | // factory.Start() releases the execution flow without waiting for all the 89 | // internal machinery to warm up. 90 | for gvr, ok := range factory.WaitForCacheSync(ctx.Done()) { 91 | if !ok { 92 | panic(fmt.Sprintf("Failed to sync cache for resource %v", gvr)) 93 | } 94 | } 95 | 96 | // Consuming the work queue with N=3 parallel worker go routines. 97 | for i := 0; i < 3; i++ { 98 | // A better way is to use wait.Until() from "k8s.io/apimachinery/pkg/util/wait" 99 | // for every worker. 100 | fmt.Printf("Starting worker %d\n", i) 101 | 102 | // worker() 103 | go func(n int) { 104 | for { 105 | // Someone said we're done? 106 | select { 107 | case <-ctx.Done(): 108 | fmt.Printf("Controller's done! Worker %d exiting...\n", n) 109 | return 110 | default: 111 | } 112 | 113 | // Obtain a piece of work. 114 | key, quit := queue.Get() 115 | if quit { 116 | fmt.Printf("Work queue has been shut down! Worker %d exiting...\n", n) 117 | return 118 | } 119 | fmt.Printf("Worker %d is about to start process new item %s.\n", n, key) 120 | 121 | // processSingleItem() - scoped to utilize defer and premature returns. 122 | func() { 123 | // Tell the queue that we are done with processing this key. 124 | // This unblocks the key for other workers and allows safe parallel 125 | // processing because two objects with the same key are never processed 126 | // in parallel. 127 | defer queue.Done(key) 128 | 129 | // YOUR CONTROLLER'S BUSINESS LOGIC GOES HERE 130 | obj, err := dynamicInformer.Lister().Get(key.(string)) 131 | if err == nil { 132 | fmt.Printf("Worker %d found ConfigMap object in informer's cahce %#v.\n", n, obj) 133 | // RECONCILE THE OBJECT - PUT YOUR BUSINESS LOGIC HERE. 134 | if n == 1 { 135 | err = fmt.Errorf("worker %d is a chronic failure", n) 136 | } 137 | } else { 138 | fmt.Printf("Worker %d got error %v while looking up ConfigMap object in informer's cache.\n", n, err) 139 | } 140 | 141 | // Handle the error if something went wrong during the execution of 142 | // the business logic. 143 | 144 | if err == nil { 145 | // The key has been handled successfully - forget about it. In particular, it 146 | // ensures that future processing of updates for this key won't be rate limited 147 | // because of errors on previous attempts. 148 | fmt.Printf("Worker %d reconciled ConfigMap %s successfully. Removing it from te queue.\n", n, key) 149 | queue.Forget(key) 150 | return 151 | } 152 | 153 | // We retry no more than K=5 times. 154 | if queue.NumRequeues(key) >= 5 { 155 | fmt.Printf("Worker %d gave up on processing %s. Removing it from the queue.\n", n, key) 156 | queue.Forget(key) 157 | return 158 | } 159 | 160 | // Re-enqueue the key rate to be (re-)processed later again. 161 | // Notice that deferred queue.Done(key) call above knows how 162 | // to deal with re-enqueueing - it marks the key as done and 163 | // then re-appends it again. 164 | fmt.Printf("Worker %d failed to process %s. Putting it back to the queue to retry later.\n", n, key) 165 | queue.AddRateLimited(key) 166 | }() 167 | } 168 | }(i) 169 | } 170 | 171 | // Create some Kubernetes objects to make the above program actually process something. 172 | cm1 := createConfigMap(client) 173 | cm2 := createConfigMap(client) 174 | cm3 := createConfigMap(client) 175 | cm4 := createConfigMap(client) 176 | cm5 := createConfigMap(client) 177 | 178 | // Delete config maps created by this test. 179 | deleteConfigMap(client, cm1) 180 | deleteConfigMap(client, cm2) 181 | deleteConfigMap(client, cm3) 182 | deleteConfigMap(client, cm4) 183 | deleteConfigMap(client, cm5) 184 | 185 | // Stay for a couple more seconds to let the program finish. 186 | time.Sleep(10 * time.Second) 187 | queue.ShutDown() 188 | cancel() 189 | time.Sleep(1 * time.Second) 190 | } 191 | 192 | func createClientOrDie() dynamic.Interface { 193 | home, err := os.UserHomeDir() 194 | if err != nil { 195 | panic(err) 196 | } 197 | 198 | config, err := clientcmd.BuildConfigFromFlags("", path.Join(home, ".kube/config")) 199 | if err != nil { 200 | panic(err.Error()) 201 | } 202 | 203 | client, err := dynamic.NewForConfig(config) 204 | if err != nil { 205 | panic(err.Error()) 206 | } 207 | 208 | return client 209 | } 210 | 211 | func createConfigMap(client dynamic.Interface) *unstructured.Unstructured { 212 | cm := &unstructured.Unstructured{ 213 | Object: map[string]interface{}{ 214 | "apiVersion": "v1", 215 | "kind": "ConfigMap", 216 | "metadata": map[string]interface{}{ 217 | "namespace": namespace, 218 | "generateName": "workqueue-", 219 | }, 220 | "data": map[string]interface{}{ 221 | "foo": "bar", 222 | }, 223 | }, 224 | } 225 | 226 | cm, err := client. 227 | Resource(ConfigMapResource). 228 | Namespace(namespace). 229 | Create(context.Background(), cm, metav1.CreateOptions{}) 230 | if err != nil { 231 | panic(err.Error()) 232 | } 233 | 234 | fmt.Printf("Created ConfigMap %s/%s\n", cm.GetNamespace(), cm.GetName()) 235 | return cm 236 | } 237 | 238 | func deleteConfigMap(client dynamic.Interface, cm *unstructured.Unstructured) { 239 | err := client. 240 | Resource(ConfigMapResource). 241 | Namespace(cm.GetNamespace()). 242 | Delete(context.Background(), cm.GetName(), metav1.DeleteOptions{}) 243 | if err != nil { 244 | panic(err.Error()) 245 | } 246 | if err != nil { 247 | panic(err.Error()) 248 | } 249 | 250 | fmt.Printf("Deleted ConfigMap %s/%s\n", cm.GetNamespace(), cm.GetName()) 251 | } 252 | --------------------------------------------------------------------------------