├── .github └── workflows │ └── gobuild.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── deployment-satus.png ├── go.mod ├── go.sum ├── internal └── platform │ ├── platform_versioner.go │ ├── platform_versioner_test.go │ ├── types.go │ └── types_test.go ├── pkg ├── olm │ ├── deployment_status.go │ ├── deployment_status_test.go │ └── types.go ├── resource │ ├── compare │ │ ├── defaults.go │ │ ├── defaults_test.go │ │ ├── map.go │ │ ├── test │ │ │ ├── external_test.go │ │ │ └── utils_test.go │ │ ├── types.go │ │ └── utils.go │ ├── detector │ │ ├── README.md │ │ ├── detector.go │ │ ├── detector_test.go │ │ ├── stateManager.go │ │ └── stateManager_test.go │ ├── read │ │ ├── reader.go │ │ └── reader_test.go │ ├── test │ │ ├── utils.go │ │ └── utils_test.go │ └── write │ │ ├── hooks │ │ └── update_hooks.go │ │ ├── writer.go │ │ └── writer_test.go ├── test │ ├── mock_imagestreamtag.go │ └── mock_service.go ├── utils │ ├── kubernetes │ │ ├── api.go │ │ ├── finalizer.go │ │ ├── kube_service.go │ │ ├── reconciler.go │ │ ├── reconciler_test.go │ │ ├── utils.go │ │ └── utils_test.go │ └── openshift │ │ ├── utils.go │ │ ├── utils_test.go │ │ ├── webconsole.go │ │ └── webconsole_test.go └── validation │ ├── schema.go │ ├── schema_sync.go │ ├── schema_sync_test.go │ └── schema_test.go └── version └── version.go /.github/workflows/gobuild.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [main, next] 6 | pull_request: 7 | branches: [main, next] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Install Go 14 | uses: actions/setup-go@v2 15 | with: 16 | go-version: 1.19.x 17 | - name: Checkout code 18 | uses: actions/checkout@v2 19 | - name: Format 20 | run: | 21 | make format 22 | git diff --exit-code */**/*.go 23 | - name: Vet 24 | run: go vet ./... 25 | - name: Unit tests 26 | run: go test ./... 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Temporary Build Files 15 | build/_output/ 16 | build/_test/ 17 | target/ 18 | *-packr.go 19 | 20 | # Created by https://www.gitignore.io/api/go,vim,emacs,visualstudiocode 21 | ### Emacs ### 22 | # -*- mode: gitignore; -*- 23 | *~ 24 | \#*\# 25 | /.emacs.desktop 26 | /.emacs.desktop.lock 27 | *.elc 28 | auto-save-list 29 | tramp 30 | .\#* 31 | # Org-mode 32 | .org-id-locations 33 | *_archive 34 | # flymake-mode 35 | *_flymake.* 36 | # eshell files 37 | /eshell/history 38 | /eshell/lastdir 39 | # elpa packages 40 | /elpa/ 41 | # reftex files 42 | *.rel 43 | # AUCTeX auto folder 44 | /auto/ 45 | # cask packages 46 | .cask/ 47 | dist/ 48 | # Flycheck 49 | flycheck_*.el 50 | # server auth directory 51 | /server/ 52 | # projectiles files 53 | .projectile 54 | projectile-bookmarks.eld 55 | # directory configuration 56 | .dir-locals.el 57 | # saveplace 58 | places 59 | # url cache 60 | url/cache/ 61 | # cedet 62 | ede-projects.el 63 | # smex 64 | smex-items 65 | # company-statistics 66 | company-statistics-cache.el 67 | # anaconda-mode 68 | anaconda-mode/ 69 | ### Vim ### 70 | # swap 71 | .sw[a-p] 72 | .*.sw[a-p] 73 | # session 74 | Session.vim 75 | # temporary 76 | .netrwhist 77 | # auto-generated tag files 78 | tags 79 | ### VisualStudioCode ### 80 | .vscode/* 81 | .history 82 | # End of https://www.gitignore.io/api/go,vim,emacs,visualstudiocode 83 | ### IntelliJ ### 84 | .idea 85 | 86 | vendor/ 87 | 88 | -------------------------------------------------------------------------------- /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 | export GOFLAGS=-mod=vendor 2 | 3 | .PHONY: all 4 | all: test 5 | 6 | .PHONY: mod 7 | mod: 8 | go mod tidy && go mod vendor 9 | 10 | .PHONY: format 11 | format: mod 12 | go fmt ./... 13 | 14 | .PHONY: vet 15 | vet: format 16 | go vet ./... 17 | 18 | .PHONY: test 19 | test: vet 20 | go test ./... 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # operator-utils library 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/RHsyseng/operator-utils)](https://goreportcard.com/report/github.com/RHsyseng/operator-utils) 4 | [![Build Status](https://travis-ci.org/RHsyseng/operator-utils.svg?branch=master)](https://travis-ci.org/RHsyseng/operator-utils) 5 | 6 | This library layers on top of the Operator SDK, having set of utilities function as a library to easily create Kubernetes operators. 7 | 8 | ## Kubernetes / OpenShift Version Support 9 | 10 | In July of 2020, our team [moved away from using the term `master`](https://www.redhat.com/en/blog/making-open-source-more-inclusive-eradicating-problematic-language) for our default branch. As a result, our branching scheme is as follows: 11 | 12 | - The [main](https://github.com/RHsyseng/operator-utils/tree/main) (default) branch supports **OCP 4.13** (K8S 1.26) 13 | - For versions of `operator-utils` targeting any release of OCP (starting with 4.2), please refer to the [tags](https://github.com/RHsyseng/operator-utils/tags) section. 14 | - tag `v1.X.Y` indicates support for OCP `vX.Y` 15 | - With each General Availability release of OCP a new tag will be created from the `v1.X.Y.x` branch then the `main` branch will point to the latest OCP version. 16 | 17 | ## Contributing to the `operator-utils` Project 18 | 19 | All bugs, tasks, fixes or enhancements should be tracked as [GitHub Issues](https://github.com/RHsyseng/operator-utils/issues) & [Pull Requests](https://github.com/RHsyseng/operator-utils/pulls). 20 | 21 | - To contribute features targeting **OCP 4.13** only, use a local feature branch based off of & targeting `origin/main` with any PR's, Reference any JIRA/GitHub issues in PR's where applicable. 22 | - To contribute features targeting **both currently supported versions**, first complete the commit/PR work targeting `main`. Once that PR is merged to `main`, create a new PR with cherry-pick of the commit targeting the branch of the specific OCP version that it should be backported to. 23 | 24 | ## Declaring operator-utils dependency 25 | 26 | Regardless of dependency framework, we suggest following the best practice of declaring any and all dependencies your project utilizes regardless of target branch, tag, or revision. 27 | 28 | With regards to `operator-utils`, please **carefully** consider the given version support information above when declaring your dependency, as depending on or defaulting to `main` branch will likely result in future build complications as our project continues to evolve and cycle minor version support. 29 | 30 | - Go.mod example specifying **REVISION**: 31 | 32 | ``` 33 | github.com/RHsyseng/operator-utils v0.0.0-20200108204558-82090ef57586 34 | ``` 35 | 36 | ## Features 37 | 38 | 1. [managing CR and CRD validation](#managing-cr-and-crd-validation) 39 | 2. [pods deployment status](#pods-deployment-status) 40 | 3. [resource comparison, adding, updating and deleting](#resource-comparison-adding-updating-and-deleting) 41 | 4. [platform detection Kubernetes VS Openshift](#platform-detection-kubernetes-vs-openshift) 42 | 43 | ## Managing CR and CRD validation 44 | 45 | Operator util library use package ``validation`` for validate the CRD and CR file, these function use as a unit test within operator 46 | 47 | **CRD validation Usage**: 48 | 49 | ```go 50 | 51 | schema := getCompleteSchema(t) 52 | missingEntries := schema.GetMissingEntries(&sampleApp{}) 53 | for _, missing := range missingEntries { 54 | if strings.HasPrefix(missing.Path, "/status") { 55 | //Not using subresources, so status is not expected to appear in CRD 56 | } else { 57 | assert.Fail(t, "Discrepancy between CRD and Struct", "Missing or incorrect schema validation at %v, expected type %v", missing.Path, missing.Type) 58 | } 59 | } 60 | ``` 61 | 62 | **CR validation Usage**: 63 | 64 | ```go 65 | schema, err := New([]byte(schemaYaml)) 66 | assert.NoError(t, err) 67 | 68 | type myAppSpec struct { 69 | Number float64 `json:"number,omitempty"` 70 | } 71 | 72 | type myApp struct { 73 | Spec myAppSpec `json:"spec,omitempty"` 74 | } 75 | 76 | cr := myApp{ 77 | Spec: myAppSpec{ 78 | Number: float64(23), 79 | }, 80 | } 81 | missingEntries := schema.GetMissingEntries(&cr) 82 | assert.Len(t, missingEntries, 0, "Expect no missing entries in CRD for this struct: %v", missingEntries) 83 | ``` 84 | 85 | A full example is provided [here](./pkg/validation/schema_sync_test.go) 86 | 87 | ## Pods deployment status 88 | 89 | showes the status of the deployment on OLM UI in the form of PI chart, as seen in below screenshot 90 | 91 | ![alt text](deployment-satus.png "pods PI chart") 92 | 93 | **Usage**: 94 | 95 | Below seen line required to add into types.go status structure 96 | 97 | ```go 98 | PodStatus olm.DeploymentStatus `json:"podStatus"` 99 | ``` 100 | 101 | Add these lines into CSV file inside statusDescriptors section: 102 | 103 | ```yaml 104 | statusDescriptors: 105 | - description: The current pods 106 | displayName: Pods Status 107 | path: podStatus 108 | x-descriptors: 109 | - "urn:alm:descriptor:com.tectonic.ui:podStatuses" 110 | ``` 111 | 112 | For DeploymentConfig deployment status: 113 | 114 | ```go 115 | var dcs []oappsv1.DeploymentConfig 116 | 117 | deploymentStatus := olm.GetDeploymentConfigStatus(dcs) 118 | if !reflect.DeepEqual(instance.Status.Deployments, deploymentStatus) { 119 | r.reqLogger.Info("Deployment status will be updated") 120 | instance.Status.Deployments = deploymentStatus 121 | err = r.client.Status().Update(context.TODO(), instance) 122 | if err != nil { 123 | r.reqLogger.Error(err, "Failed to update deployment status") 124 | return err 125 | } 126 | } 127 | 128 | ``` 129 | 130 | For StatefulSet Deployment status: 131 | 132 | ```go 133 | var status olm.DeploymentStatus 134 | sfsFound := &appsv1.StatefulSet{} 135 | 136 | err := client.Get(context.TODO(), namespacedName, sfsFound) 137 | if err == nil { 138 | status = olm.GetSingleStatefulSetStatus(*sfsFound) 139 | } else { 140 | dsFound := &appsv1.DaemonSet{} 141 | err = client.Get(context.TODO(), namespacedName, dsFound) 142 | if err == nil { 143 | status = olm.GetSingleDaemonSetStatus(*dsFound) 144 | } 145 | } 146 | 147 | ``` 148 | 149 | ## Resource comparison (adding, updating and deleting) 150 | 151 | Common function for listing, adding, updating, deleting kubernetes objects like seen below: 152 | 153 | List of objects that are deployed 154 | 155 | ```go 156 | reader := read.New(client).WithNamespace(instance.Namespace).WithOwnerObject(instance) 157 | resourceMap, err := reader.ListAll( 158 | &corev1.PersistentVolumeClaimList{}, 159 | &corev1.ServiceList{}, 160 | &appsv1.StatefulSetList{}, 161 | &routev1.RouteList{}, 162 | ) 163 | ``` 164 | 165 | Compare what's deployed with what should be deployed 166 | 167 | ```go 168 | requested := compare.NewMapBuilder().Add(requestedResources...).ResourceMap() 169 | comparator := compare.NewMapComparator() 170 | deltas := comparator.Compare(deployed, requested) 171 | ``` 172 | 173 | Adding the objects: 174 | 175 | ```go 176 | 177 | added, err := writer.AddResources(delta.Added) 178 | 179 | ``` 180 | 181 | Updating the objects: 182 | 183 | ```go 184 | updated, err := writer.UpdateResources(deployed[resourceType], delta.Updated) 185 | ``` 186 | 187 | Removing the objects: 188 | 189 | ```go 190 | removed, err := writer.RemoveResources(delta.Removed) 191 | ``` 192 | 193 | A full usage is provided [here]( https://github.com/kiegroup/kie-cloud-operator/blob/6964179113e4f57d47bead03578ae6ed8e9caa8b/pkg/controller/kieapp/kieapp_controller.go#L136-L163) 194 | 195 | ## Platform detection Kubernetes VS Openshift 196 | 197 | To detect platform whether operator is running on kuberenete or openshift or what version of openshift is using 198 | 199 | ```go 200 | info, err := pv.GetPlatformInfo(c.discoverer, c.config) 201 | ``` 202 | 203 | A full example is provided [here](./internal/platform/platform_versioner_test.go) 204 | 205 | ## Who is using this Library 206 | 207 | operator-utils is used by several Red Hat product & community operators, including the following: 208 | 209 | - [3scale APIcast Operator](https://github.com/3scale/apicast-operator) 210 | - [3scale Operator](https://github.com/3scale/3scale-operator) 211 | - [ActiveMQ Artemis Operator](https://github.com/rh-messaging/activemq-artemis-operator) 212 | - [ActiveMQ Artemis Broker Test Suite](https://github.com/artemiscloud/openshift-broker-test-suite) 213 | - [AtlasMap Operator](https://github.com/atlasmap/atlasmap-operator) 214 | - [Barometer Operator](https://github.com/aneeshkp/barometer-operator) 215 | - [Infinispan Operator](https://github.com/infinispan/infinispan-operator) 216 | - [Integreatly Operator](https://github.com/integr8ly/integreatly-operator) 217 | - [Kie Cloud Operator](https://github.com/kiegroup/kie-cloud-operator) 218 | - [Kogito Operator](https://github.com/kiegroup/kogito-cloud-operator) 219 | - [KubeDB Operator](https://github.com/mrhillsman/kubedb-operator) 220 | - [KubeVirt Containerized Data Importer](https://github.com/kubevirt/containerized-data-importer) 221 | - [KubeVirt Hyperconverged Operator](https://github.com/kubevirt/hyperconverged-cluster-operator) 222 | - [Nexus Operator](https://github.com/m88i/nexus-operator) 223 | - [OCS Meta Operator](https://github.com/openshift/ocs-operator) 224 | - [Performance Addon Operator](https://github.com/openshift-kni/performance-addon-operators) 225 | - [QDR Interconnect Operator](https://github.com/interconnectedcloud/qdr-operator) 226 | - [RHI Operator](https://github.com/redhat-integration/rhi-operator) 227 | - [Serverless Orchestration](https://github.com/RHsyseng/serverless-orchestration) 228 | - [ShipShape Testing Operator](https://github.com/rh-messaging/shipshape) 229 | - [Teiid Operator](https://github.com/teiid/teiid-operator) 230 | - [Wildfly Operator](https://github.com/wildfly/wildfly-operator) 231 | -------------------------------------------------------------------------------- /deployment-satus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RHsyseng/operator-utils/a226fabb2226a313dd3a16591c5579ebcd8a74b0/deployment-satus.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/RHsyseng/operator-utils 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/ghodss/yaml v1.0.0 7 | github.com/go-openapi/spec v0.19.9 8 | github.com/go-openapi/strfmt v0.19.5 9 | github.com/go-openapi/validate v0.19.11 10 | github.com/go-test/deep v1.1.0 11 | github.com/google/gnostic v0.5.7-v3refs 12 | github.com/openshift/api v0.0.0-20211209135129-c58d9f695577 13 | github.com/openshift/client-go v0.0.0-20211209144617-7385dd6338e3 14 | github.com/pkg/errors v0.9.1 15 | github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.55.1 16 | github.com/stretchr/testify v1.8.0 17 | k8s.io/api v0.26.6 18 | k8s.io/apimachinery v0.26.6 19 | k8s.io/client-go v0.26.6 20 | sigs.k8s.io/controller-runtime v0.14.6 21 | ) 22 | 23 | require ( 24 | github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect 25 | github.com/beorn7/perks v1.0.1 // indirect 26 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 27 | github.com/davecgh/go-spew v1.1.1 // indirect 28 | github.com/emicklei/go-restful/v3 v3.9.0 // indirect 29 | github.com/evanphx/json-patch v4.12.0+incompatible // indirect 30 | github.com/evanphx/json-patch/v5 v5.6.0 // indirect 31 | github.com/fsnotify/fsnotify v1.6.0 // indirect 32 | github.com/go-logr/logr v1.2.3 // indirect 33 | github.com/go-openapi/analysis v0.19.10 // indirect 34 | github.com/go-openapi/errors v0.19.7 // indirect 35 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 36 | github.com/go-openapi/jsonreference v0.20.0 // indirect 37 | github.com/go-openapi/loads v0.19.5 // indirect 38 | github.com/go-openapi/runtime v0.19.16 // indirect 39 | github.com/go-openapi/swag v0.19.14 // indirect 40 | github.com/go-stack/stack v1.8.0 // indirect 41 | github.com/gogo/protobuf v1.3.2 // indirect 42 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 43 | github.com/golang/protobuf v1.5.2 // indirect 44 | github.com/google/go-cmp v0.5.9 // indirect 45 | github.com/google/gofuzz v1.1.0 // indirect 46 | github.com/google/uuid v1.3.0 // indirect 47 | github.com/imdario/mergo v0.3.12 // indirect 48 | github.com/josharian/intern v1.0.0 // indirect 49 | github.com/json-iterator/go v1.1.12 // indirect 50 | github.com/mailru/easyjson v0.7.6 // indirect 51 | github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect 52 | github.com/mitchellh/mapstructure v1.4.1 // indirect 53 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 54 | github.com/modern-go/reflect2 v1.0.2 // indirect 55 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 56 | github.com/pmezard/go-difflib v1.0.0 // indirect 57 | github.com/prometheus/client_golang v1.14.0 // indirect 58 | github.com/prometheus/client_model v0.3.0 // indirect 59 | github.com/prometheus/common v0.37.0 // indirect 60 | github.com/prometheus/procfs v0.8.0 // indirect 61 | github.com/spf13/pflag v1.0.5 // indirect 62 | go.mongodb.org/mongo-driver v1.5.1 // indirect 63 | golang.org/x/net v0.8.0 // indirect 64 | golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect 65 | golang.org/x/sys v0.6.0 // indirect 66 | golang.org/x/term v0.6.0 // indirect 67 | golang.org/x/text v0.8.0 // indirect 68 | golang.org/x/time v0.3.0 // indirect 69 | gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect 70 | google.golang.org/appengine v1.6.7 // indirect 71 | google.golang.org/protobuf v1.28.1 // indirect 72 | gopkg.in/inf.v0 v0.9.1 // indirect 73 | gopkg.in/yaml.v2 v2.4.0 // indirect 74 | gopkg.in/yaml.v3 v3.0.1 // indirect 75 | k8s.io/apiextensions-apiserver v0.26.1 // indirect 76 | k8s.io/component-base v0.26.1 // indirect 77 | k8s.io/klog/v2 v2.80.1 // indirect 78 | k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect 79 | k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 // indirect 80 | sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect 81 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 82 | sigs.k8s.io/yaml v1.3.0 // indirect 83 | ) 84 | -------------------------------------------------------------------------------- /internal/platform/platform_versioner.go: -------------------------------------------------------------------------------- 1 | package platform 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | 8 | openapi_v2 "github.com/google/gnostic/openapiv2" 9 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/version" 11 | "k8s.io/client-go/discovery" 12 | "k8s.io/client-go/rest" 13 | "sigs.k8s.io/controller-runtime/pkg/client/config" 14 | logf "sigs.k8s.io/controller-runtime/pkg/log" 15 | ) 16 | 17 | var ( 18 | log = logf.Log.WithName("utils") 19 | clusterVersionAPIPath = "apis/config.openshift.io/v1/clusterversions/version" 20 | ) 21 | 22 | type PlatformVersioner interface { 23 | GetPlatformInfo(discoverer Discoverer, cfg *rest.Config) (PlatformInfo, error) 24 | } 25 | 26 | type Discoverer interface { 27 | ServerVersion() (*version.Info, error) 28 | ServerGroups() (*v1.APIGroupList, error) 29 | OpenAPISchema() (*openapi_v2.Document, error) 30 | RESTClient() rest.Interface 31 | } 32 | 33 | type K8SBasedPlatformVersioner struct{} 34 | 35 | /* 36 | MapKnownVersion maps from K8S version of PlatformInfo to equivalent OpenShift version 37 | 38 | Result: OpenShiftVersion{ Version: 4.1.2 } 39 | */ 40 | func MapKnownVersion(info PlatformInfo) OpenShiftVersion { 41 | k8sToOcpMap := map[string]string{ 42 | "1.10+": "3.10", 43 | "1.10": "3.10", 44 | "1.11+": "3.11", 45 | "1.11": "3.11", 46 | "1.13+": "4.1", 47 | "1.13": "4.1", 48 | "1.14+": "4.2", 49 | "1.14": "4.2", 50 | "1.16+": "4.3", 51 | "1.16": "4.3", 52 | "1.17+": "4.4", 53 | "1.17": "4.4", 54 | "1.18+": "4.5", 55 | "1.18": "4.5", 56 | "1.19+": "4.6", 57 | "1.19": "4.6", 58 | "1.20+": "4.7", 59 | "1.20": "4.7", 60 | "1.21+": "4.8", 61 | "1.21": "4.8", 62 | "1.22+": "4.9", 63 | "1.22": "4.9", 64 | "1.23+": "4.10", 65 | "1.23": "4.10", 66 | "1.24+": "4.11", 67 | "1.24": "4.11", 68 | "1.25+": "4.12", 69 | "1.25": "4.12", 70 | "1.26+": "4.13", 71 | "1.26": "4.13", 72 | } 73 | return OpenShiftVersion{Version: k8sToOcpMap[info.K8SVersion]} 74 | } 75 | 76 | // deal with cfg coming from legacy method signature and allow injection for client testing 77 | func (K8SBasedPlatformVersioner) DefaultArgs(client Discoverer, cfg *rest.Config) (Discoverer, *rest.Config, error) { 78 | if cfg == nil { 79 | var err error 80 | cfg, err = config.GetConfig() 81 | if err != nil { 82 | return nil, nil, err 83 | } 84 | } 85 | if client == nil { 86 | var err error 87 | client, err = discovery.NewDiscoveryClientForConfig(cfg) 88 | if err != nil { 89 | return nil, nil, err 90 | } 91 | } 92 | return client, cfg, nil 93 | } 94 | 95 | func (pv K8SBasedPlatformVersioner) GetPlatformInfo(client Discoverer, cfg *rest.Config) (PlatformInfo, error) { 96 | log.Info("detecting platform version...") 97 | info := PlatformInfo{Name: Kubernetes} 98 | 99 | var err error 100 | client, cfg, err = pv.DefaultArgs(client, cfg) 101 | if err != nil { 102 | log.Info("issue occurred while defaulting client/cfg args") 103 | return info, err 104 | } 105 | 106 | k8sVersion, err := client.ServerVersion() 107 | if err != nil { 108 | log.Info("issue occurred while fetching ServerVersion") 109 | return info, err 110 | } 111 | info.K8SVersion = k8sVersion.Major + "." + k8sVersion.Minor 112 | info.OS = k8sVersion.Platform 113 | 114 | apiList, err := client.ServerGroups() 115 | if err != nil { 116 | log.Info("issue occurred while fetching ServerGroups") 117 | return info, err 118 | } 119 | 120 | for _, v := range apiList.Groups { 121 | if v.Name == "route.openshift.io" { 122 | 123 | log.Info("route.openshift.io found in apis, platform is OpenShift") 124 | info.Name = OpenShift 125 | break 126 | } 127 | } 128 | log.Info(info.String()) 129 | return info, nil 130 | } 131 | 132 | /* 133 | OCP4.1+ requires elevated cluster configuration user security permissions for version fetch 134 | REST call URL requiring permissions: /apis/config.openshift.io/v1/clusterversions 135 | */ 136 | func (pv K8SBasedPlatformVersioner) LookupOpenShiftVersion(client Discoverer, cfg *rest.Config) (OpenShiftVersion, error) { 137 | 138 | osv := OpenShiftVersion{} 139 | client, _, err := pv.DefaultArgs(nil, nil) 140 | if err != nil { 141 | log.Info("issue occurred while defaulting args for version lookup") 142 | return osv, err 143 | } 144 | doc, err := client.OpenAPISchema() 145 | if err != nil { 146 | log.Info("issue occurred while fetching OpenAPISchema") 147 | return osv, err 148 | } 149 | 150 | switch doc.Info.Version[:4] { 151 | case "v3.1": 152 | osv.Version = doc.Info.Version 153 | 154 | // OCP4 returns K8S major/minor from old API endpoint [bugzilla-1658957] 155 | case "v1.1": 156 | rest := client.RESTClient().Get().AbsPath(clusterVersionAPIPath) 157 | 158 | result := rest.Do(context.TODO()) 159 | if result.Error() != nil { 160 | log.Info("issue making API version rest call: " + result.Error().Error()) 161 | return osv, result.Error() 162 | } 163 | 164 | // error handling before/after Raw() seems redundant, but error detail can be lost in convert 165 | body, err := result.Raw() 166 | if err != nil { 167 | log.Info("issue pulling raw result from API call") 168 | return osv, err 169 | } 170 | 171 | var cvi PlatformClusterInfo 172 | err = json.Unmarshal(body, &cvi) 173 | if err != nil { 174 | log.Info("issue occurred while unmarshalling PlatformClusterInfo") 175 | return osv, err 176 | } 177 | osv.Version = cvi.Status.Desired.Version 178 | } 179 | return osv, nil 180 | } 181 | 182 | func (pv K8SBasedPlatformVersioner) CompareOpenShiftVersion(client Discoverer, cfg *rest.Config, version string) (int, error) { 183 | info, err := pv.GetPlatformInfo(client, cfg) 184 | if err != nil { 185 | return -1, err 186 | } 187 | if !info.IsOpenShift() { 188 | return -1, errors.New("There is no OpenShift platform detected.") 189 | } 190 | curVersion := MapKnownVersion(info) 191 | return curVersion.Compare(OpenShiftVersion{Version: version}) 192 | } 193 | -------------------------------------------------------------------------------- /internal/platform/platform_versioner_test.go: -------------------------------------------------------------------------------- 1 | package platform 2 | 3 | import ( 4 | "testing" 5 | 6 | openapi_v2 "github.com/google/gnostic/openapiv2" 7 | "github.com/pkg/errors" 8 | "github.com/stretchr/testify/assert" 9 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/version" 11 | "k8s.io/client-go/rest" 12 | ) 13 | 14 | type FakeDiscoverer struct { 15 | info PlatformInfo 16 | serverInfo *version.Info 17 | groupList *v1.APIGroupList 18 | doc *openapi_v2.Document 19 | client rest.Interface 20 | ServerVersionError error 21 | ServerGroupsError error 22 | OpenAPISchemaError error 23 | } 24 | 25 | func (d FakeDiscoverer) ServerVersion() (*version.Info, error) { 26 | if d.ServerVersionError != nil { 27 | return nil, d.ServerVersionError 28 | } 29 | return d.serverInfo, nil 30 | } 31 | 32 | func (d FakeDiscoverer) ServerGroups() (*v1.APIGroupList, error) { 33 | if d.ServerGroupsError != nil { 34 | return nil, d.ServerGroupsError 35 | } 36 | return d.groupList, nil 37 | } 38 | 39 | func (d FakeDiscoverer) OpenAPISchema() (*openapi_v2.Document, error) { 40 | if d.OpenAPISchemaError != nil { 41 | return nil, d.OpenAPISchemaError 42 | } 43 | return d.doc, nil 44 | } 45 | 46 | func (d FakeDiscoverer) RESTClient() rest.Interface { 47 | return d.client 48 | } 49 | 50 | type FakePlatformVersioner struct { 51 | Info PlatformInfo 52 | Err error 53 | } 54 | 55 | func (pv FakePlatformVersioner) GetPlatformInfo(d Discoverer, cfg *rest.Config) (PlatformInfo, error) { 56 | if pv.Err != nil { 57 | return pv.Info, pv.Err 58 | } 59 | return pv.Info, nil 60 | } 61 | 62 | func TestK8SBasedPlatformVersioner_GetPlatformInfo(t *testing.T) { 63 | 64 | pv := K8SBasedPlatformVersioner{} 65 | fakeErr := errors.New("uh oh") 66 | 67 | cases := []struct { 68 | label string 69 | discoverer Discoverer 70 | config *rest.Config 71 | expectedInfo PlatformInfo 72 | expectedErr bool 73 | }{ 74 | { 75 | label: "case 1", // trigger error in client.ServerVersion(), only Name present on Info 76 | discoverer: FakeDiscoverer{ 77 | ServerVersionError: fakeErr, 78 | }, 79 | config: &rest.Config{}, 80 | expectedInfo: PlatformInfo{Name: Kubernetes}, 81 | expectedErr: true, 82 | }, 83 | { 84 | label: "case 2", // trigger error in client.ServerGroups(), K8S major/minor now present 85 | discoverer: FakeDiscoverer{ 86 | ServerGroupsError: fakeErr, 87 | serverInfo: &version.Info{ 88 | Major: "1", 89 | Minor: "2", 90 | }, 91 | }, 92 | config: &rest.Config{}, 93 | expectedInfo: PlatformInfo{Name: Kubernetes, K8SVersion: "1.2"}, 94 | expectedErr: true, 95 | }, 96 | { 97 | label: "case 3", // trigger no errors, simulate K8S platform (no OCP route present) 98 | discoverer: FakeDiscoverer{ 99 | serverInfo: &version.Info{ 100 | Major: "1", 101 | Minor: "2", 102 | }, 103 | groupList: &v1.APIGroupList{ 104 | TypeMeta: v1.TypeMeta{}, 105 | Groups: []v1.APIGroup{}, 106 | }, 107 | }, 108 | config: &rest.Config{}, 109 | expectedInfo: PlatformInfo{Name: Kubernetes, K8SVersion: "1.2"}, 110 | expectedErr: false, 111 | }, 112 | { 113 | label: "case 4", // trigger no errors, simulate OCP route present 114 | discoverer: FakeDiscoverer{ 115 | serverInfo: &version.Info{ 116 | Major: "1", 117 | Minor: "2", 118 | }, 119 | groupList: &v1.APIGroupList{ 120 | TypeMeta: v1.TypeMeta{}, 121 | Groups: []v1.APIGroup{{Name: "route.openshift.io"}}, 122 | }, 123 | }, 124 | config: &rest.Config{}, 125 | expectedInfo: PlatformInfo{Name: OpenShift, K8SVersion: "1.2"}, 126 | expectedErr: false, 127 | }, 128 | } 129 | 130 | for _, c := range cases { 131 | info, err := pv.GetPlatformInfo(c.discoverer, c.config) 132 | assert.Equal(t, c.expectedInfo, info, c.label+": mismatch in returned PlatformInfo") 133 | if c.expectedErr { 134 | assert.Error(t, err, c.label+": expected error, but none occurred") 135 | } 136 | } 137 | } 138 | 139 | func TestClientCallVersionComparsion(t *testing.T) { 140 | pv := K8SBasedPlatformVersioner{} 141 | testcases := []struct { 142 | label string 143 | discoverer Discoverer 144 | config *rest.Config 145 | expectedInfo int 146 | expectedErr bool 147 | }{ 148 | { 149 | label: "case 1", 150 | discoverer: FakeDiscoverer{ 151 | serverInfo: &version.Info{ 152 | Major: "1", 153 | Minor: "26", 154 | }, 155 | groupList: &v1.APIGroupList{ 156 | TypeMeta: v1.TypeMeta{}, 157 | Groups: []v1.APIGroup{{Name: "route.openshift.io"}}, 158 | }, 159 | }, 160 | config: &rest.Config{}, 161 | expectedInfo: 0, 162 | expectedErr: false, 163 | }, 164 | { 165 | label: "case 2", 166 | discoverer: FakeDiscoverer{ 167 | serverInfo: &version.Info{ 168 | Major: "1", 169 | Minor: "26+", 170 | }, 171 | groupList: &v1.APIGroupList{ 172 | TypeMeta: v1.TypeMeta{}, 173 | Groups: []v1.APIGroup{{Name: "route.openshift.io"}}, 174 | }, 175 | }, 176 | config: &rest.Config{}, 177 | expectedInfo: 0, 178 | expectedErr: false, 179 | }, 180 | { 181 | label: "case 3", 182 | discoverer: FakeDiscoverer{ 183 | serverInfo: &version.Info{ 184 | Major: "1", 185 | Minor: "14+", 186 | }, 187 | groupList: &v1.APIGroupList{ 188 | TypeMeta: v1.TypeMeta{}, 189 | Groups: []v1.APIGroup{{Name: "route.openshift.io"}}, 190 | }, 191 | }, 192 | config: &rest.Config{}, 193 | expectedInfo: -1, 194 | expectedErr: false, 195 | }, 196 | { 197 | label: "case 4", 198 | discoverer: FakeDiscoverer{ 199 | serverInfo: &version.Info{ 200 | Major: "1", 201 | Minor: "14not", 202 | }, 203 | groupList: &v1.APIGroupList{ 204 | TypeMeta: v1.TypeMeta{}, 205 | Groups: []v1.APIGroup{{Name: "route.openshift.io"}}, 206 | }, 207 | }, 208 | config: &rest.Config{}, 209 | expectedInfo: -1, 210 | expectedErr: true, 211 | }, 212 | } 213 | 214 | versionToTest := "4.13" 215 | for _, tc := range testcases { 216 | res, err := pv.CompareOpenShiftVersion(tc.discoverer, tc.config, versionToTest) 217 | if tc.expectedErr { 218 | assert.Error(t, err, "expected error") 219 | } else { 220 | assert.NoError(t, err, "unexpected error") 221 | } 222 | assert.Equal(t, tc.expectedInfo, res, "The expected and actual versions should be the same.") 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /internal/platform/types.go: -------------------------------------------------------------------------------- 1 | package platform 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | type PlatformType string 10 | 11 | const ( 12 | OpenShift PlatformType = "OpenShift" 13 | Kubernetes PlatformType = "Kubernetes" 14 | ) 15 | 16 | type PlatformInfo struct { 17 | Name PlatformType `json:"name"` 18 | K8SVersion string `json:"k8sVersion"` 19 | OS string `json:"os"` 20 | } 21 | 22 | func (info PlatformInfo) K8SMajorVersion() string { 23 | return strings.Split(info.K8SVersion, ".")[0] 24 | } 25 | 26 | func (info PlatformInfo) K8SMinorVersion() string { 27 | return strings.Split(info.K8SVersion, ".")[1] 28 | } 29 | 30 | func (info PlatformInfo) IsOpenShift() bool { 31 | return info.Name == OpenShift 32 | } 33 | 34 | func (info PlatformInfo) IsKubernetes() bool { 35 | return info.Name == Kubernetes 36 | } 37 | 38 | func (info PlatformInfo) String() string { 39 | return "PlatformInfo [" + 40 | "Name: " + fmt.Sprintf("%v", info.Name) + 41 | ", K8SVersion: " + info.K8SVersion + 42 | ", OS: " + info.OS + "]" 43 | } 44 | 45 | type OpenShiftVersion struct { 46 | Version string `json:"ocpVersion"` 47 | } 48 | 49 | func (info OpenShiftVersion) MajorVersion() string { 50 | return strings.Split(info.Version, ".")[0] 51 | } 52 | 53 | func (info OpenShiftVersion) MinorVersion() string { 54 | return strings.Split(info.Version, ".")[1] 55 | } 56 | 57 | func (info OpenShiftVersion) BuildVersion() string { 58 | return strings.Join(strings.Split(info.Version, ".")[2:], ".") 59 | } 60 | 61 | func (info OpenShiftVersion) String() string { 62 | return "OpenShiftVersion [" + 63 | "Version: " + info.Version + "]" 64 | } 65 | 66 | func (v OpenShiftVersion) Compare(o OpenShiftVersion) (int, error) { 67 | if d, err := compareSegment(v.MajorVersion(), o.MajorVersion()); d != 0 { 68 | return d, err 69 | } 70 | if d, err := compareSegment(v.MinorVersion(), o.MinorVersion()); d != 0 { 71 | return d, err 72 | } 73 | return 0, nil 74 | } 75 | 76 | func compareSegment(v, o string) (int, error) { 77 | v1, err := strconv.Atoi(v) 78 | if err != nil { 79 | return -1, err 80 | } 81 | v2, err := strconv.Atoi(o) 82 | if err != nil { 83 | return -1, err 84 | } 85 | if v1 < v2 { 86 | return -1, nil 87 | } 88 | if v1 > v2 { 89 | return 1, nil 90 | } 91 | return 0, nil 92 | } 93 | 94 | // full generated 'version' API fetch result struct @ 95 | // gist.github.com/jeremyary/5a66530611572a057df7a98f3d2902d5 96 | type PlatformClusterInfo struct { 97 | Status struct { 98 | Desired struct { 99 | Version string `json:"version"` 100 | } `json:"desired"` 101 | } `json:"status"` 102 | } 103 | -------------------------------------------------------------------------------- /internal/platform/types_test.go: -------------------------------------------------------------------------------- 1 | package platform 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestK8SVersionHelpers(t *testing.T) { 9 | 10 | ocpTestVersions := []struct { 11 | version string 12 | major string 13 | minor string 14 | }{ 15 | {"1.11+", "1", "11+"}, 16 | {"1.13+", "1", "13+"}, 17 | } 18 | 19 | for _, v := range ocpTestVersions { 20 | 21 | info := PlatformInfo{K8SVersion: v.version} 22 | assert.Equal(t, v.major, info.K8SMajorVersion(), "K8SMajorVersion mismatch") 23 | assert.Equal(t, v.minor, info.K8SMinorVersion(), "K8SMinorVersion mismatch") 24 | } 25 | } 26 | 27 | func TestPlatformInfo_String(t *testing.T) { 28 | 29 | info := PlatformInfo{Name: OpenShift, K8SVersion: "456", OS: "foo/bar"} 30 | 31 | assert.Equal(t, "PlatformInfo [Name: OpenShift, K8SVersion: 456, OS: foo/bar]", 32 | info.String(), "PlatformInfo String() yields malformed result of %s", info.String()) 33 | } 34 | 35 | func TestVersionHelpers(t *testing.T) { 36 | 37 | ocpTestVersions := []struct { 38 | version string 39 | major string 40 | minor string 41 | build string 42 | }{ 43 | {"3.11.69", "3", "11", "69"}, 44 | {"4.1.0-rc.1", "4", "1", "0-rc.1"}, 45 | {"1.2.3.4.5.6", "1", "2", "3.4.5.6"}, 46 | {"1.2", "1", "2", ""}, 47 | } 48 | 49 | for _, v := range ocpTestVersions { 50 | 51 | info := OpenShiftVersion{Version: v.version} 52 | assert.Equal(t, v.major, info.MajorVersion(), "OCPMajorVersion mismatch") 53 | assert.Equal(t, v.minor, info.MinorVersion(), "OCPMinorVersion mismatch") 54 | assert.Equal(t, v.build, info.BuildVersion(), "OCPBuildVersion mismatch") 55 | } 56 | } 57 | 58 | func TestOpenShiftVersion_String(t *testing.T) { 59 | 60 | info := OpenShiftVersion{Version: "1.1.1+"} 61 | 62 | assert.Equal(t, "OpenShiftVersion [Version: 1.1.1+]", 63 | info.String(), "OpenShiftVersion String() yields malformed result of %s", info.String()) 64 | } 65 | 66 | func TestVersionComparsion(t *testing.T) { 67 | targetVersions := []OpenShiftVersion{ 68 | OpenShiftVersion{Version: "3.11"}, 69 | OpenShiftVersion{Version: "4.1"}, 70 | OpenShiftVersion{Version: "4.3"}, 71 | OpenShiftVersion{Version: "4.4"}, 72 | OpenShiftVersion{Version: "4.3fail"}, 73 | } 74 | 75 | currOCPVersion := OpenShiftVersion{Version: "4.3.1"} 76 | res, err := currOCPVersion.Compare(targetVersions[0]) 77 | assert.NoError(t, err) 78 | assert.Equal(t, 1, res, "cur. ocp version should be bigger than target.") 79 | 80 | res, err = currOCPVersion.Compare(targetVersions[1]) 81 | assert.NoError(t, err) 82 | assert.Equal(t, 1, res, "cur. ocp version should be bigger than target.") 83 | 84 | res, err = currOCPVersion.Compare(targetVersions[2]) 85 | assert.NoError(t, err) 86 | assert.Equal(t, 0, res, "cur. ocp version should be the same as target.") 87 | 88 | res, err = currOCPVersion.Compare(targetVersions[3]) 89 | assert.NoError(t, err) 90 | assert.Equal(t, -1, res, "cur. ocp version should be smaller than target.") 91 | 92 | res, err = currOCPVersion.Compare(targetVersions[4]) 93 | assert.Error(t, err, "There should be a parsing error.") 94 | } 95 | -------------------------------------------------------------------------------- /pkg/olm/deployment_status.go: -------------------------------------------------------------------------------- 1 | package olm 2 | 3 | import ( 4 | "fmt" 5 | oappsv1 "github.com/openshift/api/apps/v1" 6 | appsv1 "k8s.io/api/apps/v1" 7 | logf "sigs.k8s.io/controller-runtime/pkg/log" 8 | ) 9 | 10 | var log = logf.Log.WithName("olm") 11 | 12 | func GetDaemonSetStatus(dcs []appsv1.DaemonSet) DeploymentStatus { 13 | return getDeploymentStatus(deploymentsWrapper{ 14 | countFunc: func() int { 15 | return len(dcs) 16 | }, 17 | nameFunc: func(i int) string { 18 | return dcs[i].Name 19 | }, 20 | requestedReplicasFunc: func(i int) int32 { 21 | //DaemonSet means an implicit replica count request of one per node, return >0: 22 | return 1 23 | }, 24 | targetReplicasFunc: func(i int) int32 { 25 | return dcs[i].Status.DesiredNumberScheduled 26 | }, 27 | readyReplicasFunc: func(i int) int32 { 28 | return dcs[i].Status.NumberReady 29 | }, 30 | }) 31 | } 32 | 33 | func GetDeploymentStatus(dcs []appsv1.Deployment) DeploymentStatus { 34 | return getDeploymentStatus(deploymentsWrapper{ 35 | countFunc: func() int { 36 | return len(dcs) 37 | }, 38 | nameFunc: func(i int) string { 39 | return dcs[i].Name 40 | }, 41 | requestedReplicasFunc: func(i int) int32 { 42 | return getInt32(dcs[i].Spec.Replicas) 43 | }, 44 | targetReplicasFunc: func(i int) int32 { 45 | return dcs[i].Status.Replicas 46 | }, 47 | readyReplicasFunc: func(i int) int32 { 48 | return dcs[i].Status.ReadyReplicas 49 | }, 50 | }) 51 | } 52 | 53 | func GetDeploymentConfigStatus(dcs []oappsv1.DeploymentConfig) DeploymentStatus { 54 | return getDeploymentStatus(deploymentsWrapper{ 55 | countFunc: func() int { 56 | return len(dcs) 57 | }, 58 | nameFunc: func(i int) string { 59 | return dcs[i].Name 60 | }, 61 | requestedReplicasFunc: func(i int) int32 { 62 | return dcs[i].Spec.Replicas 63 | }, 64 | targetReplicasFunc: func(i int) int32 { 65 | return dcs[i].Status.Replicas 66 | }, 67 | readyReplicasFunc: func(i int) int32 { 68 | return dcs[i].Status.ReadyReplicas 69 | }, 70 | }) 71 | } 72 | 73 | func getDeploymentStatus(obj deployments) DeploymentStatus { 74 | var ready, starting, stopped []string 75 | for i := 0; i < obj.count(); i++ { 76 | if obj.requestedReplicas(i) == 0 { 77 | stopped = append(stopped, obj.name(i)) 78 | } else if obj.targetReplicas(i) == 0 { 79 | stopped = append(stopped, obj.name(i)) 80 | } else if obj.readyReplicas(i) < obj.targetReplicas(i) { 81 | starting = append(starting, obj.name(i)) 82 | } else { 83 | ready = append(ready, obj.name(i)) 84 | } 85 | } 86 | log.Info("Found deployments with status ", "stopped", stopped, "starting", starting, "ready", ready) 87 | return DeploymentStatus{ 88 | Stopped: stopped, 89 | Starting: starting, 90 | Ready: ready, 91 | } 92 | 93 | } 94 | 95 | func GetSingleDaemonSetStatus(ds appsv1.DaemonSet) DeploymentStatus { 96 | return getSingleDeploymentStatus(ds.Name, 1, ds.Status.DesiredNumberScheduled, ds.Status.NumberReady) 97 | } 98 | 99 | func GetSingleDeploymentStatus(dc appsv1.Deployment) DeploymentStatus { 100 | return getSingleDeploymentStatus(dc.Name, getInt32(dc.Spec.Replicas), dc.Status.Replicas, dc.Status.ReadyReplicas) 101 | } 102 | 103 | func GetSingleStatefulSetStatus(ss appsv1.StatefulSet) DeploymentStatus { 104 | return getSingleDeploymentStatus(ss.Name, getInt32(ss.Spec.Replicas), ss.Status.Replicas, ss.Status.ReadyReplicas) 105 | } 106 | 107 | func getInt32(pointer *int32) int32 { 108 | if pointer == nil { 109 | return 0 110 | } else { 111 | return *pointer 112 | } 113 | 114 | } 115 | func getSingleDeploymentStatus(name string, requestedCount int32, targetCount int32, readyCount int32) DeploymentStatus { 116 | var ready, starting, stopped []string 117 | if requestedCount == 0 || targetCount == 0 { 118 | stopped = append(stopped, name) 119 | } else { 120 | for i := int32(0); i < targetCount; i++ { 121 | instanceName := fmt.Sprintf("%s-%d", name, i+1) 122 | if i < readyCount { 123 | ready = append(ready, instanceName) 124 | } else { 125 | starting = append(starting, instanceName) 126 | } 127 | } 128 | } 129 | log.Info("Found deployments with status ", "stopped", stopped, "starting", starting, "ready", ready) 130 | return DeploymentStatus{ 131 | Stopped: stopped, 132 | Starting: starting, 133 | Ready: ready, 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /pkg/olm/deployment_status_test.go: -------------------------------------------------------------------------------- 1 | package olm 2 | 3 | import ( 4 | oappsv1 "github.com/openshift/api/apps/v1" 5 | "github.com/stretchr/testify/assert" 6 | appsv1 "k8s.io/api/apps/v1" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "testing" 9 | ) 10 | 11 | func TestDaemonSetStatus(t *testing.T) { 12 | objs := []appsv1.DaemonSet{ 13 | { 14 | ObjectMeta: metav1.ObjectMeta{ 15 | Name: "StoppedDeployment", 16 | }, 17 | Status: appsv1.DaemonSetStatus{ 18 | DesiredNumberScheduled: 0, 19 | NumberReady: 0, 20 | }, 21 | }, 22 | { 23 | ObjectMeta: metav1.ObjectMeta{ 24 | Name: "StartingDeployment", 25 | }, 26 | Status: appsv1.DaemonSetStatus{ 27 | DesiredNumberScheduled: 3, 28 | NumberReady: 1, 29 | }, 30 | }, 31 | { 32 | ObjectMeta: metav1.ObjectMeta{ 33 | Name: "ReadyDeployment", 34 | }, 35 | Status: appsv1.DaemonSetStatus{ 36 | DesiredNumberScheduled: 3, 37 | NumberReady: 3, 38 | }, 39 | }, 40 | } 41 | status := GetDaemonSetStatus(objs) 42 | assert.Len(t, status.Stopped, 1, "Expected one stopped deployment") 43 | assert.Equal(t, "StoppedDeployment", status.Stopped[0]) 44 | assert.Len(t, status.Starting, 1, "Expected one starting deployment") 45 | assert.Equal(t, "StartingDeployment", status.Starting[0]) 46 | assert.Len(t, status.Ready, 1, "Expected one ready deployment") 47 | assert.Equal(t, "ReadyDeployment", status.Ready[0]) 48 | } 49 | 50 | func TestDeploymentsStatus(t *testing.T) { 51 | zero := int32(0) 52 | three := int32(3) 53 | objs := []appsv1.Deployment{ 54 | { 55 | ObjectMeta: metav1.ObjectMeta{ 56 | Name: "StoppedDeployment", 57 | }, 58 | Spec: appsv1.DeploymentSpec{ 59 | Replicas: &zero, 60 | }, 61 | Status: appsv1.DeploymentStatus{ 62 | Replicas: 0, 63 | ReadyReplicas: 0, 64 | }, 65 | }, 66 | { 67 | ObjectMeta: metav1.ObjectMeta{ 68 | Name: "StoppedDeployment2", 69 | }, 70 | Spec: appsv1.DeploymentSpec{ 71 | Replicas: &three, 72 | }, 73 | Status: appsv1.DeploymentStatus{ 74 | Replicas: 0, 75 | ReadyReplicas: 0, 76 | }, 77 | }, 78 | { 79 | ObjectMeta: metav1.ObjectMeta{ 80 | Name: "StartingDeployment", 81 | }, 82 | Spec: appsv1.DeploymentSpec{ 83 | Replicas: &three, 84 | }, 85 | Status: appsv1.DeploymentStatus{ 86 | Replicas: three, 87 | ReadyReplicas: 1, 88 | }, 89 | }, 90 | { 91 | ObjectMeta: metav1.ObjectMeta{ 92 | Name: "ReadyDeployment", 93 | }, 94 | Spec: appsv1.DeploymentSpec{ 95 | Replicas: &three, 96 | }, 97 | Status: appsv1.DeploymentStatus{ 98 | Replicas: three, 99 | ReadyReplicas: three, 100 | }, 101 | }, 102 | } 103 | status := GetDeploymentStatus(objs) 104 | assert.Len(t, status.Stopped, 2, "Expected two stopped deployment") 105 | assert.Equal(t, "StoppedDeployment", status.Stopped[0]) 106 | assert.Equal(t, "StoppedDeployment2", status.Stopped[1]) 107 | assert.Len(t, status.Starting, 1, "Expected one starting deployment") 108 | assert.Equal(t, "StartingDeployment", status.Starting[0]) 109 | assert.Len(t, status.Ready, 1, "Expected one ready deployment") 110 | assert.Equal(t, "ReadyDeployment", status.Ready[0]) 111 | } 112 | 113 | func TestDeploymentConfigsStatus(t *testing.T) { 114 | objs := []oappsv1.DeploymentConfig{ 115 | { 116 | ObjectMeta: metav1.ObjectMeta{ 117 | Name: "StoppedDeployment", 118 | }, 119 | Spec: oappsv1.DeploymentConfigSpec{ 120 | Replicas: 0, 121 | }, 122 | Status: oappsv1.DeploymentConfigStatus{ 123 | Replicas: 0, 124 | ReadyReplicas: 0, 125 | }, 126 | }, 127 | { 128 | ObjectMeta: metav1.ObjectMeta{ 129 | Name: "StoppedDeployment2", 130 | }, 131 | Spec: oappsv1.DeploymentConfigSpec{ 132 | Replicas: 3, 133 | }, 134 | Status: oappsv1.DeploymentConfigStatus{ 135 | Replicas: 0, 136 | ReadyReplicas: 0, 137 | }, 138 | }, 139 | { 140 | ObjectMeta: metav1.ObjectMeta{ 141 | Name: "StartingDeployment", 142 | }, 143 | Spec: oappsv1.DeploymentConfigSpec{ 144 | Replicas: 3, 145 | }, 146 | Status: oappsv1.DeploymentConfigStatus{ 147 | Replicas: 3, 148 | ReadyReplicas: 1, 149 | }, 150 | }, 151 | { 152 | ObjectMeta: metav1.ObjectMeta{ 153 | Name: "ReadyDeployment", 154 | }, 155 | Spec: oappsv1.DeploymentConfigSpec{ 156 | Replicas: 3, 157 | }, 158 | Status: oappsv1.DeploymentConfigStatus{ 159 | Replicas: 3, 160 | ReadyReplicas: 3, 161 | }, 162 | }, 163 | } 164 | status := GetDeploymentConfigStatus(objs) 165 | assert.Len(t, status.Stopped, 2, "Expected two stopped deployment") 166 | assert.Equal(t, "StoppedDeployment", status.Stopped[0]) 167 | assert.Equal(t, "StoppedDeployment2", status.Stopped[1]) 168 | assert.Len(t, status.Starting, 1, "Expected one starting deployment") 169 | assert.Equal(t, "StartingDeployment", status.Starting[0]) 170 | assert.Len(t, status.Ready, 1, "Expected one ready deployment") 171 | assert.Equal(t, "ReadyDeployment", status.Ready[0]) 172 | } 173 | 174 | func TestSingleDaemonSetStatus(t *testing.T) { 175 | obj := appsv1.DaemonSet{ 176 | ObjectMeta: metav1.ObjectMeta{ 177 | Name: "ReadyDeployment", 178 | }, 179 | Status: appsv1.DaemonSetStatus{ 180 | DesiredNumberScheduled: 3, 181 | NumberReady: 3, 182 | }, 183 | } 184 | status := GetSingleDaemonSetStatus(obj) 185 | assert.Len(t, status.Stopped, 0, "Expected no stopped deployments") 186 | assert.Len(t, status.Starting, 0, "Expected no starting deployments") 187 | assert.Len(t, status.Ready, 3, "Expected three ready deployments") 188 | assert.Equal(t, "ReadyDeployment-1", status.Ready[0]) 189 | assert.Equal(t, "ReadyDeployment-2", status.Ready[1]) 190 | assert.Equal(t, "ReadyDeployment-3", status.Ready[2]) 191 | } 192 | 193 | func TestStartingSingleDaemonSetStatus(t *testing.T) { 194 | obj := appsv1.DaemonSet{ 195 | ObjectMeta: metav1.ObjectMeta{ 196 | Name: "StartingDeployment", 197 | }, 198 | Status: appsv1.DaemonSetStatus{ 199 | DesiredNumberScheduled: 3, 200 | NumberReady: 1, 201 | }, 202 | } 203 | status := GetSingleDaemonSetStatus(obj) 204 | assert.Len(t, status.Stopped, 0, "Expected no stopped deployments") 205 | assert.Len(t, status.Ready, 1, "Expected one ready deployment") 206 | assert.Equal(t, "StartingDeployment-1", status.Ready[0]) 207 | assert.Len(t, status.Starting, 2, "Expected two starting deployments") 208 | assert.Equal(t, "StartingDeployment-2", status.Starting[0]) 209 | assert.Equal(t, "StartingDeployment-3", status.Starting[1]) 210 | } 211 | 212 | func TestStoppedSingleDaemonSetStatus(t *testing.T) { 213 | obj := appsv1.DaemonSet{ 214 | ObjectMeta: metav1.ObjectMeta{ 215 | Name: "StoppedDeployment", 216 | }, 217 | Status: appsv1.DaemonSetStatus{ 218 | DesiredNumberScheduled: 0, 219 | NumberReady: 0, 220 | }, 221 | } 222 | status := GetSingleDaemonSetStatus(obj) 223 | assert.Len(t, status.Stopped, 1, "Expected one stopped deployment") 224 | assert.Equal(t, "StoppedDeployment", status.Stopped[0]) 225 | assert.Len(t, status.Starting, 0, "Expected no starting deployments") 226 | assert.Len(t, status.Ready, 0, "Expected no ready deployments") 227 | } 228 | 229 | func TestSingleDeploymentStatus(t *testing.T) { 230 | three := int32(3) 231 | obj := appsv1.Deployment{ 232 | ObjectMeta: metav1.ObjectMeta{ 233 | Name: "ReadyDeployment", 234 | }, 235 | Spec: appsv1.DeploymentSpec{ 236 | Replicas: &three, 237 | }, 238 | Status: appsv1.DeploymentStatus{ 239 | Replicas: 3, 240 | ReadyReplicas: 3, 241 | }, 242 | } 243 | status := GetSingleDeploymentStatus(obj) 244 | assert.Len(t, status.Stopped, 0, "Expected no stopped deployments") 245 | assert.Len(t, status.Starting, 0, "Expected no starting deployments") 246 | assert.Len(t, status.Ready, 3, "Expected three ready deployments") 247 | assert.Equal(t, "ReadyDeployment-1", status.Ready[0]) 248 | assert.Equal(t, "ReadyDeployment-2", status.Ready[1]) 249 | assert.Equal(t, "ReadyDeployment-3", status.Ready[2]) 250 | } 251 | 252 | func TestStartingSingleDeploymentStatus(t *testing.T) { 253 | three := int32(3) 254 | obj := appsv1.Deployment{ 255 | ObjectMeta: metav1.ObjectMeta{ 256 | Name: "StartingDeployment", 257 | }, 258 | Spec: appsv1.DeploymentSpec{ 259 | Replicas: &three, 260 | }, 261 | Status: appsv1.DeploymentStatus{ 262 | Replicas: 3, 263 | ReadyReplicas: 1, 264 | }, 265 | } 266 | status := GetSingleDeploymentStatus(obj) 267 | assert.Len(t, status.Stopped, 0, "Expected no stopped deployments") 268 | assert.Len(t, status.Ready, 1, "Expected one ready deployment") 269 | assert.Equal(t, "StartingDeployment-1", status.Ready[0]) 270 | assert.Len(t, status.Starting, 2, "Expected two starting deployments") 271 | assert.Equal(t, "StartingDeployment-2", status.Starting[0]) 272 | assert.Equal(t, "StartingDeployment-3", status.Starting[1]) 273 | } 274 | 275 | func TestStoppedSingleDeploymentStatus(t *testing.T) { 276 | zero := int32(0) 277 | obj := appsv1.Deployment{ 278 | ObjectMeta: metav1.ObjectMeta{ 279 | Name: "StoppedDeployment", 280 | }, 281 | Spec: appsv1.DeploymentSpec{ 282 | Replicas: &zero, 283 | }, 284 | Status: appsv1.DeploymentStatus{ 285 | Replicas: 0, 286 | ReadyReplicas: 0, 287 | }, 288 | } 289 | status := GetSingleDeploymentStatus(obj) 290 | assert.Len(t, status.Stopped, 1, "Expected one stopped deployment") 291 | assert.Equal(t, "StoppedDeployment", status.Stopped[0]) 292 | assert.Len(t, status.Starting, 0, "Expected no starting deployments") 293 | assert.Len(t, status.Ready, 0, "Expected no ready deployments") 294 | } 295 | 296 | func TestSingleStatefulSetStatus(t *testing.T) { 297 | three := int32(3) 298 | obj := appsv1.StatefulSet{ 299 | ObjectMeta: metav1.ObjectMeta{ 300 | Name: "ReadyDeployment", 301 | }, 302 | Spec: appsv1.StatefulSetSpec{ 303 | Replicas: &three, 304 | }, 305 | Status: appsv1.StatefulSetStatus{ 306 | Replicas: 3, 307 | ReadyReplicas: 3, 308 | }, 309 | } 310 | status := GetSingleStatefulSetStatus(obj) 311 | assert.Len(t, status.Stopped, 0, "Expected no stopped deployments") 312 | assert.Len(t, status.Starting, 0, "Expected no starting deployments") 313 | assert.Len(t, status.Ready, 3, "Expected three ready deployments") 314 | assert.Equal(t, "ReadyDeployment-1", status.Ready[0]) 315 | assert.Equal(t, "ReadyDeployment-2", status.Ready[1]) 316 | assert.Equal(t, "ReadyDeployment-3", status.Ready[2]) 317 | } 318 | 319 | func TestStartingSingleStatefulSetStatus(t *testing.T) { 320 | three := int32(3) 321 | obj := appsv1.StatefulSet{ 322 | ObjectMeta: metav1.ObjectMeta{ 323 | Name: "StartingDeployment", 324 | }, 325 | Spec: appsv1.StatefulSetSpec{ 326 | Replicas: &three, 327 | }, 328 | Status: appsv1.StatefulSetStatus{ 329 | Replicas: 3, 330 | ReadyReplicas: 1, 331 | }, 332 | } 333 | status := GetSingleStatefulSetStatus(obj) 334 | assert.Len(t, status.Stopped, 0, "Expected no stopped deployments") 335 | assert.Len(t, status.Ready, 1, "Expected one ready deployment") 336 | assert.Equal(t, "StartingDeployment-1", status.Ready[0]) 337 | assert.Len(t, status.Starting, 2, "Expected two starting deployments") 338 | assert.Equal(t, "StartingDeployment-2", status.Starting[0]) 339 | assert.Equal(t, "StartingDeployment-3", status.Starting[1]) 340 | } 341 | 342 | func TestStoppedSingleStatefulSetStatus(t *testing.T) { 343 | zero := int32(0) 344 | obj := appsv1.StatefulSet{ 345 | ObjectMeta: metav1.ObjectMeta{ 346 | Name: "StoppedDeployment", 347 | }, 348 | Spec: appsv1.StatefulSetSpec{ 349 | Replicas: &zero, 350 | }, 351 | Status: appsv1.StatefulSetStatus{ 352 | Replicas: 0, 353 | ReadyReplicas: 0, 354 | }, 355 | } 356 | status := GetSingleStatefulSetStatus(obj) 357 | assert.Len(t, status.Stopped, 1, "Expected one stopped deployment") 358 | assert.Equal(t, "StoppedDeployment", status.Stopped[0]) 359 | assert.Len(t, status.Starting, 0, "Expected no starting deployments") 360 | assert.Len(t, status.Ready, 0, "Expected no ready deployments") 361 | } 362 | -------------------------------------------------------------------------------- /pkg/olm/types.go: -------------------------------------------------------------------------------- 1 | package olm 2 | 3 | type DeploymentStatus struct { 4 | // Deployments are ready to serve requests 5 | Ready []string `json:"ready,omitempty"` 6 | // Deployments are starting, may or may not succeed 7 | Starting []string `json:"starting,omitempty"` 8 | // Deployments are not starting, unclear what next step will be 9 | Stopped []string `json:"stopped,omitempty"` 10 | } 11 | 12 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 13 | func (in *DeploymentStatus) DeepCopyInto(out *DeploymentStatus) { 14 | *out = *in 15 | if in.Ready != nil { 16 | in, out := &in.Ready, &out.Ready 17 | *out = make([]string, len(*in)) 18 | copy(*out, *in) 19 | } 20 | if in.Starting != nil { 21 | in, out := &in.Starting, &out.Starting 22 | *out = make([]string, len(*in)) 23 | copy(*out, *in) 24 | } 25 | if in.Stopped != nil { 26 | in, out := &in.Stopped, &out.Stopped 27 | *out = make([]string, len(*in)) 28 | copy(*out, *in) 29 | } 30 | return 31 | } 32 | 33 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Deployments. 34 | func (in *DeploymentStatus) DeepCopy() *DeploymentStatus { 35 | if in == nil { 36 | return nil 37 | } 38 | out := new(DeploymentStatus) 39 | in.DeepCopyInto(out) 40 | return out 41 | } 42 | 43 | type deployments interface { 44 | count() int 45 | name(i int) string 46 | requestedReplicas(i int) int32 47 | targetReplicas(i int) int32 48 | readyReplicas(i int) int32 49 | } 50 | 51 | type deploymentsWrapper struct { 52 | countFunc func() int 53 | nameFunc func(i int) string 54 | requestedReplicasFunc func(i int) int32 55 | targetReplicasFunc func(i int) int32 56 | readyReplicasFunc func(i int) int32 57 | } 58 | 59 | func (obj deploymentsWrapper) count() int { 60 | return obj.countFunc() 61 | } 62 | 63 | func (obj deploymentsWrapper) name(i int) string { 64 | return obj.nameFunc(i) 65 | } 66 | 67 | func (obj deploymentsWrapper) requestedReplicas(i int) int32 { 68 | return obj.requestedReplicasFunc(i) 69 | } 70 | 71 | func (obj deploymentsWrapper) targetReplicas(i int) int32 { 72 | return obj.targetReplicasFunc(i) 73 | } 74 | 75 | func (obj deploymentsWrapper) readyReplicas(i int) int32 { 76 | return obj.readyReplicasFunc(i) 77 | } 78 | -------------------------------------------------------------------------------- /pkg/resource/compare/defaults.go: -------------------------------------------------------------------------------- 1 | package compare 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "sort" 7 | "strings" 8 | 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | 11 | "github.com/go-test/deep" 12 | oappsv1 "github.com/openshift/api/apps/v1" 13 | buildv1 "github.com/openshift/api/build/v1" 14 | routev1 "github.com/openshift/api/route/v1" 15 | appsv1 "k8s.io/api/apps/v1" 16 | corev1 "k8s.io/api/core/v1" 17 | rbacv1 "k8s.io/api/rbac/v1" 18 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 19 | ) 20 | 21 | const ( 22 | imageTriggersAnnotation = "image.openshift.io/triggers" 23 | deploymentRevisionAnnotation = "deployment.kubernetes.io/revision" 24 | imageTriggerContainerNameValueFmt = "spec.template.spec.containers[?(@.name==\\\"%s\\\")].image" 25 | ) 26 | 27 | type resourceComparator struct { 28 | defaultCompareFunc func(deployed client.Object, requested client.Object) bool 29 | compareFuncMap map[reflect.Type]func(deployed client.Object, requested client.Object) bool 30 | } 31 | 32 | func (this *resourceComparator) SetDefaultComparator(compFunc func(deployed client.Object, requested client.Object) bool) { 33 | this.defaultCompareFunc = compFunc 34 | } 35 | 36 | func (this *resourceComparator) GetDefaultComparator() func(deployed client.Object, requested client.Object) bool { 37 | return this.defaultCompareFunc 38 | } 39 | 40 | func (this *resourceComparator) SetComparator(resourceType reflect.Type, compFunc func(deployed client.Object, requested client.Object) bool) { 41 | this.compareFuncMap[resourceType] = compFunc 42 | } 43 | 44 | func (this *resourceComparator) GetComparator(resourceType reflect.Type) func(deployed client.Object, requested client.Object) bool { 45 | return this.compareFuncMap[resourceType] 46 | } 47 | 48 | func (this *resourceComparator) Compare(deployed client.Object, requested client.Object) bool { 49 | compareFunc := this.GetDefaultComparator() 50 | type1 := reflect.ValueOf(deployed).Elem().Type() 51 | type2 := reflect.ValueOf(requested).Elem().Type() 52 | if type1 == type2 { 53 | if comparator, exists := this.compareFuncMap[type1]; exists { 54 | compareFunc = comparator 55 | } 56 | } 57 | return compareFunc(deployed, requested) 58 | } 59 | 60 | func (this *resourceComparator) CompareArrays(deployed []client.Object, requested []client.Object) ResourceDelta { 61 | deployedMap := getObjectMap(deployed) 62 | requestedMap := getObjectMap(requested) 63 | var added []client.Object 64 | var updated []client.Object 65 | var removed []client.Object 66 | for name, requestedObject := range requestedMap { 67 | deployedObject := deployedMap[name] 68 | if deployedObject == nil { 69 | added = append(added, requestedObject) 70 | } else if !this.Compare(deployedObject, requestedObject) { 71 | updated = append(updated, requestedObject) 72 | } 73 | } 74 | for name, deployedObject := range deployedMap { 75 | if requestedMap[name] == nil { 76 | removed = append(removed, deployedObject) 77 | } 78 | } 79 | return ResourceDelta{ 80 | Added: added, 81 | Updated: updated, 82 | Removed: removed, 83 | } 84 | } 85 | 86 | func getObjectMap(objects []client.Object) map[string]client.Object { 87 | objectMap := make(map[string]client.Object) 88 | for index := range objects { 89 | objectMap[objects[index].GetName()] = objects[index] 90 | } 91 | return objectMap 92 | } 93 | 94 | func defaultMap() map[reflect.Type]func(deployed client.Object, requested client.Object) bool { 95 | equalsMap := make(map[reflect.Type]func(client.Object, client.Object) bool) 96 | equalsMap[reflect.TypeOf(oappsv1.DeploymentConfig{})] = equalDeploymentConfigs 97 | equalsMap[reflect.TypeOf(appsv1.Deployment{})] = equalDeployment 98 | equalsMap[reflect.TypeOf(corev1.Service{})] = equalServices 99 | equalsMap[reflect.TypeOf(routev1.Route{})] = equalRoutes 100 | equalsMap[reflect.TypeOf(rbacv1.Role{})] = equalRoles 101 | equalsMap[reflect.TypeOf(rbacv1.RoleBinding{})] = equalRoleBindings 102 | equalsMap[reflect.TypeOf(corev1.ServiceAccount{})] = equalServiceAccounts 103 | equalsMap[reflect.TypeOf(corev1.Secret{})] = equalSecrets 104 | equalsMap[reflect.TypeOf(buildv1.BuildConfig{})] = equalBuildConfigs 105 | return equalsMap 106 | } 107 | 108 | func equalDeploymentConfigs(deployed client.Object, requested client.Object) bool { 109 | dc1 := deployed.(*oappsv1.DeploymentConfig) 110 | dc2 := requested.(*oappsv1.DeploymentConfig) 111 | 112 | //Removed generated fields from deployed version, when not specified in requested item 113 | dc1 = dc1.DeepCopy() 114 | triggerBasedImage := make(map[string]bool) 115 | if dc2.Spec.Strategy.RecreateParams == nil { 116 | dc1.Spec.Strategy.RecreateParams = nil 117 | } 118 | if dc2.Spec.Strategy.ActiveDeadlineSeconds == nil { 119 | dc1.Spec.Strategy.ActiveDeadlineSeconds = nil 120 | } 121 | if dc2.Spec.Strategy.RollingParams == nil && dc2.Spec.Strategy.Type == "" && dc1.Spec.Strategy.Type == oappsv1.DeploymentStrategyTypeRolling { 122 | //This looks like a default generated strategy that should be ignored 123 | dc1.Spec.Strategy.Type = "" 124 | dc1.Spec.Strategy.RollingParams = nil 125 | } 126 | if dc1.Spec.Strategy.RollingParams != nil && dc2.Spec.Strategy.RollingParams != nil { 127 | if dc2.Spec.Strategy.RollingParams.UpdatePeriodSeconds == nil { 128 | dc1.Spec.Strategy.RollingParams.UpdatePeriodSeconds = nil 129 | } 130 | if dc2.Spec.Strategy.RollingParams.IntervalSeconds == nil { 131 | dc1.Spec.Strategy.RollingParams.IntervalSeconds = nil 132 | } 133 | if dc2.Spec.Strategy.RollingParams.TimeoutSeconds == nil { 134 | dc1.Spec.Strategy.RollingParams.TimeoutSeconds = nil 135 | } 136 | if dc2.Spec.Strategy.RollingParams.MaxUnavailable == nil { 137 | dc1.Spec.Strategy.RollingParams.MaxUnavailable = nil 138 | } 139 | if dc2.Spec.Strategy.RollingParams.MaxSurge == nil { 140 | dc1.Spec.Strategy.RollingParams.MaxSurge = nil 141 | } 142 | } 143 | if dc2.Spec.RevisionHistoryLimit == nil { 144 | dc1.Spec.RevisionHistoryLimit = nil 145 | } 146 | if len(dc1.Spec.Triggers) == 1 && len(dc2.Spec.Triggers) == 0 { 147 | defaultTrigger := oappsv1.DeploymentTriggerPolicy{Type: oappsv1.DeploymentTriggerOnConfigChange} 148 | if dc1.Spec.Triggers[0] == defaultTrigger { 149 | //Remove default generated trigger 150 | dc1.Spec.Triggers = nil 151 | } 152 | } 153 | for i := range dc1.Spec.Triggers { 154 | if len(dc2.Spec.Triggers) <= i { 155 | logger.Info("No matching trigger found in requested DC", "deployed.DC.trigger", dc1.Spec.Triggers[i]) 156 | return false 157 | } 158 | if dc1.Spec.Triggers[i].ImageChangeParams != nil && dc2.Spec.Triggers[i].ImageChangeParams != nil { 159 | if dc2.Spec.Triggers[i].ImageChangeParams.LastTriggeredImage == "" { 160 | dc1.Spec.Triggers[i].ImageChangeParams.LastTriggeredImage = "" 161 | } 162 | for _, containerName := range dc2.Spec.Triggers[i].ImageChangeParams.ContainerNames { 163 | triggerBasedImage[containerName] = true 164 | } 165 | } 166 | } 167 | if !checkGeneratePodValues(dc1.Spec.Template, dc2.Spec.Template, triggerBasedImage) { 168 | return false 169 | } 170 | ignoreEmptyMaps(dc1, dc2) 171 | sortDeploymentVars(dc1.Spec.Template, dc2.Spec.Template) 172 | 173 | var pairs [][2]interface{} 174 | pairs = append(pairs, [2]interface{}{dc1.Name, dc2.Name}) 175 | pairs = append(pairs, [2]interface{}{dc1.Namespace, dc2.Namespace}) 176 | pairs = append(pairs, [2]interface{}{dc1.Labels, dc2.Labels}) 177 | pairs = append(pairs, [2]interface{}{dc1.Annotations, dc2.Annotations}) 178 | pairs = append(pairs, [2]interface{}{dc1.Spec, dc2.Spec}) 179 | equal := EqualPairs(pairs) 180 | if !equal { 181 | if logger.GetSink().Enabled(1) { 182 | logger.V(1).Info("Resources are not equal", "deployed", deployed, "requested", requested) 183 | } else { 184 | logger.Info("Resources are not equal. For more details set the Operator log level to DEBUG.") 185 | } 186 | } 187 | return equal 188 | } 189 | 190 | func equalDeployment(deployed client.Object, requested client.Object) bool { 191 | d1 := deployed.(*appsv1.Deployment) 192 | d2 := requested.(*appsv1.Deployment) 193 | 194 | d1 = d1.DeepCopy() 195 | triggerBasedImage := make(map[string]bool) 196 | 197 | if d2.Spec.Strategy.RollingUpdate == nil && d1.Spec.Strategy.RollingUpdate != nil { 198 | d1.Spec.Strategy.RollingUpdate = nil 199 | } 200 | if d1.Spec.Strategy.RollingUpdate != nil && d2.Spec.Strategy.RollingUpdate != nil { 201 | if d2.Spec.Strategy.RollingUpdate.MaxSurge == nil { 202 | d1.Spec.Strategy.RollingUpdate.MaxSurge = nil 203 | } 204 | if d2.Spec.Strategy.RollingUpdate.MaxUnavailable == nil { 205 | d1.Spec.Strategy.RollingUpdate.MaxUnavailable = nil 206 | } 207 | } 208 | if d2.Spec.RevisionHistoryLimit == nil { 209 | d1.Spec.RevisionHistoryLimit = nil 210 | } 211 | if d2.Spec.ProgressDeadlineSeconds == nil { 212 | d1.Spec.ProgressDeadlineSeconds = nil 213 | } 214 | 215 | if &d1.Spec.Template != nil && &d2.Spec.Template != nil { 216 | if v, ok := d1.Annotations[imageTriggersAnnotation]; ok { 217 | for _, container := range d1.Spec.Template.Spec.Containers { 218 | if strings.Contains(v, fmt.Sprintf(imageTriggerContainerNameValueFmt, container.Name)) { 219 | triggerBasedImage[container.Name] = true 220 | } 221 | } 222 | for _, initContainer := range d1.Spec.Template.Spec.InitContainers { 223 | if strings.Contains(v, fmt.Sprintf(imageTriggerContainerNameValueFmt, initContainer.Name)) { 224 | triggerBasedImage[initContainer.Name] = true 225 | } 226 | } 227 | } 228 | if !checkGeneratePodValues(&d1.Spec.Template, &d2.Spec.Template, triggerBasedImage) { 229 | return false 230 | } 231 | } 232 | 233 | delete(d1.Annotations, deploymentRevisionAnnotation) 234 | 235 | ignoreEmptyMaps(d1, d2) 236 | sortDeploymentVars(&d1.Spec.Template, &d2.Spec.Template) 237 | 238 | var pairs [][2]interface{} 239 | pairs = append(pairs, [2]interface{}{d1.Name, d2.Name}) 240 | pairs = append(pairs, [2]interface{}{d1.Namespace, d2.Namespace}) 241 | pairs = append(pairs, [2]interface{}{d1.Labels, d2.Labels}) 242 | pairs = append(pairs, [2]interface{}{d1.Annotations, d2.Annotations}) 243 | pairs = append(pairs, [2]interface{}{d1.Spec, d2.Spec}) 244 | equal := EqualPairs(pairs) 245 | if !equal { 246 | logger.V(1).Info("Resources are not equal", "deployed", deployed, "requested", requested) 247 | } 248 | return equal 249 | } 250 | 251 | func sortBuildConfigVars(bc1 *buildv1.BuildConfig, bc2 *buildv1.BuildConfig) { 252 | if &bc1.Spec.Strategy == nil || &bc2.Spec.Strategy == nil { 253 | return 254 | } 255 | if bc1.Spec.Strategy.CustomStrategy != nil && bc2.Spec.Strategy.CustomStrategy != nil { 256 | sortEnvVars(bc1.Spec.Strategy.CustomStrategy.Env, bc2.Spec.Strategy.CustomStrategy.Env) 257 | } 258 | if bc1.Spec.Strategy.DockerStrategy != nil && bc2.Spec.Strategy.DockerStrategy != nil { 259 | sortEnvVars(bc1.Spec.Strategy.DockerStrategy.Env, bc2.Spec.Strategy.DockerStrategy.Env) 260 | } 261 | if bc1.Spec.Strategy.JenkinsPipelineStrategy != nil && bc2.Spec.Strategy.JenkinsPipelineStrategy != nil { 262 | sortEnvVars(bc1.Spec.Strategy.JenkinsPipelineStrategy.Env, bc2.Spec.Strategy.JenkinsPipelineStrategy.Env) 263 | } 264 | if bc1.Spec.Strategy.SourceStrategy != nil && bc2.Spec.Strategy.SourceStrategy != nil { 265 | sortEnvVars(bc1.Spec.Strategy.SourceStrategy.Env, bc2.Spec.Strategy.SourceStrategy.Env) 266 | } 267 | } 268 | 269 | func sortDeploymentVars(pod1 *corev1.PodTemplateSpec, pod2 *corev1.PodTemplateSpec) { 270 | for index := range pod1.Spec.Containers { 271 | if len(pod2.Spec.Containers) <= index { 272 | logger.Info("No matching container found in requested resource", "deployed container", pod1.Spec.Containers[index]) 273 | return 274 | } 275 | sortEnvVars(pod1.Spec.Containers[index].Env, pod2.Spec.Containers[index].Env) 276 | } 277 | 278 | for index := range pod1.Spec.InitContainers { 279 | if len(pod2.Spec.InitContainers) <= index { 280 | logger.Info("No matching init container found in requested resource", "deployed container", pod1.Spec.InitContainers[index]) 281 | return 282 | } 283 | sortEnvVars(pod1.Spec.InitContainers[index].Env, pod2.Spec.InitContainers[index].Env) 284 | } 285 | } 286 | 287 | func sortEnvVars(envs1 []corev1.EnvVar, envs2 []corev1.EnvVar) { 288 | sort.Slice(envs1, func(i, j int) bool { 289 | return envs1[i].Name < envs1[j].Name 290 | }) 291 | sort.Slice(envs2, func(i, j int) bool { 292 | return envs2[i].Name < envs2[j].Name 293 | }) 294 | } 295 | 296 | func checkGeneratePodValues(pod1 *corev1.PodTemplateSpec, pod2 *corev1.PodTemplateSpec, triggerBasedImage map[string]bool) bool { 297 | if pod1 != nil && pod2 != nil { 298 | for i := range pod1.Spec.Volumes { 299 | if len(pod2.Spec.Volumes) <= i { 300 | logger.Info("No matching volume found in requested deployment", "deployed.deployment.volume", pod1.Spec.Volumes[i]) 301 | return false 302 | } 303 | volSrc1 := pod1.Spec.Volumes[i].VolumeSource 304 | volSrc2 := pod2.Spec.Volumes[i].VolumeSource 305 | if volSrc1.Secret != nil && volSrc2.Secret != nil && volSrc2.Secret.DefaultMode == nil { 306 | volSrc1.Secret.DefaultMode = nil 307 | } 308 | } 309 | if pod2.Spec.RestartPolicy == "" { 310 | pod1.Spec.RestartPolicy = "" 311 | } 312 | if pod2.Spec.DNSPolicy == "" { 313 | pod1.Spec.DNSPolicy = "" 314 | } 315 | //noinspection GoDeprecation 316 | if pod2.Spec.DeprecatedServiceAccount == "" { 317 | pod1.Spec.DeprecatedServiceAccount = "" 318 | } 319 | if pod2.Spec.SecurityContext == nil { 320 | pod1.Spec.SecurityContext = nil 321 | } 322 | if pod2.Spec.SchedulerName == "" { 323 | pod1.Spec.SchedulerName = "" 324 | } 325 | ignoreGenerateContainerValues(pod1.Spec.Containers, pod2.Spec.Containers, triggerBasedImage) 326 | ignoreGenerateContainerValues(pod1.Spec.InitContainers, pod2.Spec.InitContainers, triggerBasedImage) 327 | if pod2.Spec.TerminationGracePeriodSeconds == nil { 328 | pod1.Spec.TerminationGracePeriodSeconds = nil 329 | } 330 | } 331 | return true 332 | } 333 | 334 | func ignoreGenerateContainerValues(containers1 []corev1.Container, containers2 []corev1.Container, triggerBasedImage map[string]bool) { 335 | for i := range containers1 { 336 | if len(containers2) <= i { 337 | logger.Info("No matching container found in requested resource", "deployed container", containers1[i]) 338 | return 339 | } 340 | if containers2[i].ImagePullPolicy == "" && containers1[i].ImagePullPolicy == corev1.PullAlways { 341 | containers1[i].ImagePullPolicy = "" 342 | } 343 | probes1 := []*corev1.Probe{containers1[i].LivenessProbe, containers1[i].ReadinessProbe} 344 | probes2 := []*corev1.Probe{containers2[i].LivenessProbe, containers2[i].ReadinessProbe} 345 | for index := range probes1 { 346 | probe1 := probes1[index] 347 | probe2 := probes2[index] 348 | if probe1 != nil && probe2 != nil { 349 | if probe2.FailureThreshold == 0 { 350 | probe1.FailureThreshold = probe2.FailureThreshold 351 | } 352 | if probe2.SuccessThreshold == 0 { 353 | probe1.SuccessThreshold = probe2.SuccessThreshold 354 | } 355 | if probe2.PeriodSeconds == 0 { 356 | probe1.PeriodSeconds = probe2.PeriodSeconds 357 | } 358 | if probe2.TimeoutSeconds == 0 { 359 | probe1.TimeoutSeconds = probe2.TimeoutSeconds 360 | } 361 | } 362 | } 363 | if containers2[i].TerminationMessagePath == "" { 364 | containers1[i].TerminationMessagePath = "" 365 | } 366 | if containers2[i].TerminationMessagePolicy == "" { 367 | containers1[i].TerminationMessagePolicy = "" 368 | } 369 | for j := range containers1[i].Env { 370 | if len(containers2[i].Env) <= j { 371 | return 372 | } 373 | valueFrom := containers2[i].Env[j].ValueFrom 374 | if valueFrom != nil && valueFrom.FieldRef != nil && valueFrom.FieldRef.APIVersion == "" { 375 | valueFrom1 := containers1[i].Env[j].ValueFrom 376 | if valueFrom1 != nil && valueFrom1.FieldRef != nil { 377 | valueFrom1.FieldRef.APIVersion = "" 378 | } 379 | } 380 | } 381 | if triggerBasedImage[containers1[i].Name] { 382 | //Image is being derived from the ImageChange trigger, so this image field is auto-generated after deployment 383 | containers1[i].Image = containers2[i].Image 384 | } 385 | } 386 | 387 | } 388 | 389 | func equalServices(deployed client.Object, requested client.Object) bool { 390 | service1 := deployed.(*corev1.Service) 391 | service2 := requested.(*corev1.Service) 392 | 393 | //Removed generated fields from deployed version, when not specified in requested item 394 | service1 = service1.DeepCopy() 395 | 396 | //Removed potentially generated annotations for cert request 397 | delete(service1.Annotations, "service.alpha.openshift.io/serving-cert-signed-by") 398 | delete(service1.Annotations, "service.beta.openshift.io/serving-cert-signed-by") 399 | if service2.Spec.ClusterIP == "" { 400 | service1.Spec.ClusterIP = "" 401 | } 402 | if service2.Spec.Type == "" { 403 | service1.Spec.Type = "" 404 | } 405 | if service2.Spec.SessionAffinity == "" { 406 | service1.Spec.SessionAffinity = "" 407 | } 408 | 409 | if len(service2.Spec.ClusterIPs) == 0 { 410 | service1.Spec.ClusterIPs = nil 411 | } 412 | if len(service2.Spec.IPFamilies) == 0 { 413 | service1.Spec.IPFamilies = nil 414 | } 415 | if service2.Spec.IPFamilyPolicy == nil { 416 | service1.Spec.IPFamilyPolicy = nil 417 | } 418 | if service2.Spec.InternalTrafficPolicy == nil { 419 | service1.Spec.InternalTrafficPolicy = nil 420 | } 421 | 422 | for _, port2 := range service2.Spec.Ports { 423 | if found, port1 := findServicePort(port2, service1.Spec.Ports); found { 424 | if port2.Protocol == "" { 425 | port1.Protocol = "" 426 | } 427 | } 428 | } 429 | ignoreEmptyMaps(service1, service2) 430 | 431 | var pairs [][2]interface{} 432 | pairs = append(pairs, [2]interface{}{service1.Name, service2.Name}) 433 | pairs = append(pairs, [2]interface{}{service1.Namespace, service2.Namespace}) 434 | pairs = append(pairs, [2]interface{}{service1.Labels, service2.Labels}) 435 | pairs = append(pairs, [2]interface{}{service1.Annotations, service2.Annotations}) 436 | pairs = append(pairs, [2]interface{}{service1.Spec, service2.Spec}) 437 | equal := EqualPairs(pairs) 438 | if !equal { 439 | if logger.GetSink().Enabled(1) { 440 | logger.V(1).Info("Resources are not equal", "deployed", deployed, "requested", requested) 441 | } else { 442 | logger.Info("Resources are not equal. For more details set the Operator log level to DEBUG.") 443 | } 444 | } 445 | return equal 446 | } 447 | 448 | func findServicePort(port corev1.ServicePort, ports []corev1.ServicePort) (bool, *corev1.ServicePort) { 449 | for index, candidate := range ports { 450 | if port.Name == candidate.Name { 451 | return true, &ports[index] 452 | } 453 | } 454 | return false, &corev1.ServicePort{} 455 | } 456 | 457 | func equalRoutes(deployed client.Object, requested client.Object) bool { 458 | route1 := deployed.(*routev1.Route) 459 | route2 := requested.(*routev1.Route) 460 | route1 = route1.DeepCopy() 461 | 462 | //Removed generated fields from deployed version, that are not specified in requested item 463 | delete(route1.GetAnnotations(), "openshift.io/host.generated") 464 | if route2.Spec.Host == "" { 465 | route1.Spec.Host = "" 466 | } 467 | if route2.Spec.To.Kind == "" { 468 | route1.Spec.To.Kind = "" 469 | } 470 | if route2.Spec.To.Name == "" { 471 | route1.Spec.To.Name = "" 472 | } 473 | if route2.Spec.To.Weight == nil { 474 | route1.Spec.To.Weight = nil 475 | } 476 | if route2.Spec.WildcardPolicy == "" { 477 | route1.Spec.WildcardPolicy = "" 478 | } 479 | ignoreEmptyMaps(route1, route2) 480 | 481 | var pairs [][2]interface{} 482 | pairs = append(pairs, [2]interface{}{route1.Name, route2.Name}) 483 | pairs = append(pairs, [2]interface{}{route1.Namespace, route2.Namespace}) 484 | pairs = append(pairs, [2]interface{}{route1.Labels, route2.Labels}) 485 | pairs = append(pairs, [2]interface{}{route1.Annotations, route2.Annotations}) 486 | pairs = append(pairs, [2]interface{}{route1.Spec, route2.Spec}) 487 | equal := EqualPairs(pairs) 488 | if !equal { 489 | if logger.GetSink().Enabled(1) { 490 | logger.V(1).Info("Resources are not equal", "deployed", deployed, "requested", requested) 491 | } else { 492 | logger.Info("Resources are not equal. For more details set the Operator log level to DEBUG.") 493 | } 494 | } 495 | return equal 496 | } 497 | 498 | func equalRoles(deployed client.Object, requested client.Object) bool { 499 | role1 := deployed.(*rbacv1.Role) 500 | role2 := requested.(*rbacv1.Role) 501 | var pairs [][2]interface{} 502 | pairs = append(pairs, [2]interface{}{role1.Name, role2.Name}) 503 | pairs = append(pairs, [2]interface{}{role1.Namespace, role2.Namespace}) 504 | pairs = append(pairs, [2]interface{}{role1.Labels, role2.Labels}) 505 | pairs = append(pairs, [2]interface{}{role1.Annotations, role2.Annotations}) 506 | pairs = append(pairs, [2]interface{}{role1.Rules, role2.Rules}) 507 | equal := EqualPairs(pairs) 508 | if !equal { 509 | if logger.GetSink().Enabled(1) { 510 | logger.V(1).Info("Resources are not equal", "deployed", deployed, "requested", requested) 511 | } else { 512 | logger.Info("Resources are not equal. For more details set the Operator log level to DEBUG.") 513 | } 514 | } 515 | return equal 516 | } 517 | 518 | func equalServiceAccounts(deployed client.Object, requested client.Object) bool { 519 | sa1 := deployed.(*corev1.ServiceAccount) 520 | sa2 := requested.(*corev1.ServiceAccount) 521 | var pairs [][2]interface{} 522 | pairs = append(pairs, [2]interface{}{sa1.Name, sa2.Name}) 523 | pairs = append(pairs, [2]interface{}{sa1.Namespace, sa2.Namespace}) 524 | pairs = append(pairs, [2]interface{}{sa1.Labels, sa2.Labels}) 525 | pairs = append(pairs, [2]interface{}{sa1.Annotations, sa2.Annotations}) 526 | equal := EqualPairs(pairs) 527 | if !equal { 528 | if logger.GetSink().Enabled(1) { 529 | logger.V(1).Info("Resources are not equal", "deployed", deployed, "requested", requested) 530 | } else { 531 | logger.Info("Resources are not equal. For more details set the Operator log level to DEBUG.") 532 | } 533 | } 534 | return equal 535 | } 536 | 537 | func equalRoleBindings(deployed client.Object, requested client.Object) bool { 538 | binding1 := deployed.(*rbacv1.RoleBinding) 539 | binding2 := requested.(*rbacv1.RoleBinding) 540 | var pairs [][2]interface{} 541 | pairs = append(pairs, [2]interface{}{binding1.Name, binding2.Name}) 542 | pairs = append(pairs, [2]interface{}{binding1.Namespace, binding2.Namespace}) 543 | pairs = append(pairs, [2]interface{}{binding1.Labels, binding2.Labels}) 544 | pairs = append(pairs, [2]interface{}{binding1.Annotations, binding2.Annotations}) 545 | pairs = append(pairs, [2]interface{}{binding1.Subjects, binding2.Subjects}) 546 | pairs = append(pairs, [2]interface{}{binding1.RoleRef.Name, binding2.RoleRef.Name}) 547 | equal := EqualPairs(pairs) 548 | if !equal { 549 | if logger.GetSink().Enabled(1) { 550 | logger.V(1).Info("Resources are not equal", "deployed", deployed, "requested", requested) 551 | } else { 552 | logger.Info("Resources are not equal. For more details set the Operator log level to DEBUG.") 553 | } 554 | } 555 | return equal 556 | } 557 | 558 | func equalSecrets(deployed client.Object, requested client.Object) bool { 559 | secret1 := deployed.(*corev1.Secret) 560 | secret2 := requested.(*corev1.Secret) 561 | secret1 = mergeSecretStringDataToData(secret1) 562 | secret2 = mergeSecretStringDataToData(secret2) 563 | var pairs [][2]interface{} 564 | pairs = append(pairs, [2]interface{}{secret1.Name, secret2.Name}) 565 | pairs = append(pairs, [2]interface{}{secret1.Namespace, secret2.Namespace}) 566 | pairs = append(pairs, [2]interface{}{secret1.Labels, secret2.Labels}) 567 | pairs = append(pairs, [2]interface{}{secret1.Annotations, secret2.Annotations}) 568 | pairs = append(pairs, [2]interface{}{secret1.Data, secret2.Data}) 569 | equal := EqualPairs(pairs) 570 | if !equal { 571 | if logger.GetSink().Enabled(1) { 572 | logger.V(1).Info("Resources are not equal", "deployed", deployed, "requested", requested) 573 | } else { 574 | logger.Info("Resources are not equal. For more details set the Operator log level to DEBUG.") 575 | } 576 | } 577 | return equal 578 | } 579 | 580 | func mergeSecretStringDataToData(secret *corev1.Secret) *corev1.Secret { 581 | s := secret.DeepCopy() 582 | // StringData overwrites Data 583 | if len(s.StringData) > 0 { 584 | if s.Data == nil { 585 | s.Data = map[string][]byte{} 586 | } 587 | for k, v := range s.StringData { 588 | s.Data[k] = []byte(v) 589 | } 590 | } 591 | return s 592 | } 593 | 594 | func equalBuildConfigs(deployed client.Object, requested client.Object) bool { 595 | bc1 := deployed.(*buildv1.BuildConfig) 596 | bc2 := requested.(*buildv1.BuildConfig) 597 | 598 | //Removed generated fields from deployed version, when not specified in requested item 599 | bc1 = bc1.DeepCopy() 600 | bc2 = bc2.DeepCopy() 601 | if bc2.Spec.RunPolicy == "" { 602 | bc1.Spec.RunPolicy = "" 603 | } 604 | for i := range bc1.Spec.Triggers { 605 | if len(bc1.Spec.Triggers) <= i { 606 | return false 607 | } 608 | trigger1 := bc1.Spec.Triggers[i] 609 | trigger2 := bc2.Spec.Triggers[i] 610 | webHooks1 := []*buildv1.WebHookTrigger{trigger1.BitbucketWebHook, trigger1.GenericWebHook, trigger1.GitHubWebHook, trigger1.GitLabWebHook} 611 | webHooks2 := []*buildv1.WebHookTrigger{trigger2.BitbucketWebHook, trigger2.GenericWebHook, trigger2.GitHubWebHook, trigger2.GitLabWebHook} 612 | for index := range webHooks1 { 613 | if webHooks1[index] != nil && webHooks2[index] != nil { 614 | //noinspection GoDeprecation 615 | if webHooks1[index].Secret != "" && webHooks2[index].Secret != "" { 616 | webHooks1[index].Secret = "" 617 | webHooks2[index].Secret = "" 618 | } 619 | if webHooks1[index].SecretReference != nil && webHooks2[index].SecretReference != nil { 620 | webHooks1[index].SecretReference = nil 621 | webHooks2[index].SecretReference = nil 622 | } 623 | } 624 | } 625 | if trigger2.ImageChange != nil && trigger1.ImageChange != nil { 626 | if trigger2.ImageChange.LastTriggeredImageID == "" { 627 | trigger1.ImageChange.LastTriggeredImageID = "" 628 | } 629 | } 630 | } 631 | if bc2.Spec.SuccessfulBuildsHistoryLimit == nil { 632 | bc1.Spec.SuccessfulBuildsHistoryLimit = nil 633 | } 634 | if bc2.Spec.FailedBuildsHistoryLimit == nil { 635 | bc1.Spec.FailedBuildsHistoryLimit = nil 636 | } 637 | ignoreEmptyMaps(bc1, bc2) 638 | sortBuildConfigVars(bc1, bc2) 639 | 640 | var pairs [][2]interface{} 641 | pairs = append(pairs, [2]interface{}{bc1.Name, bc2.Name}) 642 | pairs = append(pairs, [2]interface{}{bc1.Namespace, bc2.Namespace}) 643 | pairs = append(pairs, [2]interface{}{bc1.Labels, bc2.Labels}) 644 | pairs = append(pairs, [2]interface{}{bc1.Annotations, bc2.Annotations}) 645 | pairs = append(pairs, [2]interface{}{bc1.Spec, bc2.Spec}) 646 | equal := EqualPairs(pairs) 647 | if !equal { 648 | if logger.GetSink().Enabled(1) { 649 | logger.V(1).Info("Resources are not equal", "deployed", deployed, "requested", requested) 650 | } else { 651 | logger.Info("Resources are not equal. For more details set the Operator log level to DEBUG.") 652 | } 653 | } 654 | return equal 655 | } 656 | 657 | func deepEquals(deployed client.Object, requested client.Object) bool { 658 | struct1 := reflect.ValueOf(deployed).Elem().Type() 659 | if field1, found1 := struct1.FieldByName("Spec"); found1 { 660 | struct2 := reflect.ValueOf(requested).Elem().Type() 661 | if field2, found2 := struct2.FieldByName("Spec"); found2 { 662 | return Equals(field1, field2) 663 | } 664 | } 665 | return Equals(deployed, requested) 666 | } 667 | 668 | func EqualPairs(objects [][2]interface{}) bool { 669 | for index := range objects { 670 | if !Equals(objects[index][0], objects[index][1]) { 671 | return false 672 | } 673 | } 674 | return true 675 | } 676 | 677 | func Equals(deployed interface{}, requested interface{}) bool { 678 | diffs := deep.Equal(deployed, requested) 679 | equal := len(diffs) == 0 680 | if !equal { 681 | if logger.GetSink().Enabled(1) { 682 | logger.V(1).Info("Objects are not equal", "deployed", deployed, "requested", requested, "diffs", diffs) 683 | } else { 684 | logger.Info("Objects are not equal. For more details set the Operator log level to DEBUG.") 685 | } 686 | } 687 | return equal 688 | } 689 | 690 | func ignoreEmptyMaps(deployed metav1.Object, requested metav1.Object) { 691 | if requested.GetAnnotations() == nil && deployed.GetAnnotations() != nil && len(deployed.GetAnnotations()) == 0 { 692 | deployed.SetAnnotations(nil) 693 | } 694 | if requested.GetLabels() == nil && deployed.GetLabels() != nil && len(deployed.GetLabels()) == 0 { 695 | deployed.SetLabels(nil) 696 | } 697 | } 698 | -------------------------------------------------------------------------------- /pkg/resource/compare/defaults_test.go: -------------------------------------------------------------------------------- 1 | package compare 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | 8 | utils "github.com/RHsyseng/operator-utils/pkg/resource/test" 9 | oappsv1 "github.com/openshift/api/apps/v1" 10 | obuildv1 "github.com/openshift/api/build/v1" 11 | routev1 "github.com/openshift/api/route/v1" 12 | "github.com/stretchr/testify/assert" 13 | appsv1 "k8s.io/api/apps/v1" 14 | corev1 "k8s.io/api/core/v1" 15 | ) 16 | 17 | func TestCompareRoutes(t *testing.T) { 18 | routes := utils.GetRoutes(2) 19 | routes[0].Status = routev1.RouteStatus{ 20 | Ingress: []routev1.RouteIngress{ 21 | { 22 | Host: "localhost", 23 | }, 24 | }, 25 | } 26 | routes[1].Name = routes[0].Name 27 | 28 | assert.False(t, reflect.DeepEqual(routes[0], routes[1]), "Inconsequential differences between two routes should make equality test fail") 29 | assert.True(t, deepEquals(&routes[0], &routes[1]), "Expected resources to be deemed equal") 30 | assert.True(t, equalRoutes(&routes[0], &routes[1]), "Expected resources to be deemed equal based on route comparator") 31 | } 32 | 33 | func TestCompareServices(t *testing.T) { 34 | services := utils.GetServices(2) 35 | services[0].Status = corev1.ServiceStatus{ 36 | LoadBalancer: corev1.LoadBalancerStatus{ 37 | Ingress: []corev1.LoadBalancerIngress{ 38 | { 39 | IP: "127.0.0.1", 40 | Hostname: "localhost", 41 | }, 42 | }, 43 | }, 44 | } 45 | services[1].Name = services[0].Name 46 | 47 | assert.False(t, reflect.DeepEqual(services[0], services[1]), "Inconsequential differences between two services should make equality test fail") 48 | assert.True(t, deepEquals(&services[0], &services[1]), "Expected resources to be deemed equal") 49 | assert.True(t, equalServices(&services[0], &services[1]), "Expected resources to be deemed equal based on service comparator") 50 | } 51 | 52 | func TestCompareDeploymentConfigs(t *testing.T) { 53 | dcs := utils.GetDeploymentConfigs(2) 54 | dcs[1].Name = dcs[0].Name 55 | dcs[1].Status = oappsv1.DeploymentConfigStatus{ 56 | ReadyReplicas: 1, 57 | } 58 | 59 | assert.False(t, reflect.DeepEqual(dcs[0], dcs[1]), "Inconsequential differences between two DCs should make equality test fail") 60 | assert.True(t, deepEquals(&dcs[0], &dcs[1]), "Expected resources to be deemed equal") 61 | assert.True(t, equalDeploymentConfigs(&dcs[0], &dcs[1]), "Expected resources to be deemed equal based on DC comparator") 62 | } 63 | 64 | func TestCompareEmptyAnnotations(t *testing.T) { 65 | routes := utils.GetRoutes(2) 66 | routes[1].Name = routes[0].Name 67 | routes[0].Annotations = make(map[string]string) 68 | routes[0].Annotations["openshift.io/host.generated"] = "true" 69 | routes[1].Annotations = nil 70 | assert.True(t, equalRoutes(&routes[0], &routes[1]), "Routes should be considered equal") 71 | } 72 | 73 | func TestCompareDeploymentConfigLastTriggeredImage(t *testing.T) { 74 | dcs := utils.GetDeploymentConfigs(2) 75 | dcs[1].Name = dcs[0].Name 76 | dcs[0].Spec.Triggers = []oappsv1.DeploymentTriggerPolicy{ 77 | { 78 | ImageChangeParams: &oappsv1.DeploymentTriggerImageChangeParams{ 79 | Automatic: false, 80 | ContainerNames: nil, 81 | From: corev1.ObjectReference{}, 82 | LastTriggeredImage: "some generated value", 83 | }, 84 | }, 85 | } 86 | dcs[1].Spec.Triggers = []oappsv1.DeploymentTriggerPolicy{ 87 | { 88 | ImageChangeParams: &oappsv1.DeploymentTriggerImageChangeParams{ 89 | Automatic: false, 90 | ContainerNames: nil, 91 | From: corev1.ObjectReference{}, 92 | }, 93 | }, 94 | } 95 | assert.True(t, equalDeploymentConfigs(&dcs[0], &dcs[1]), "Expected resources to be deemed equal based on DC comparator") 96 | } 97 | 98 | func TestCompareDeploymentConfigImageChange(t *testing.T) { 99 | dcs := utils.GetDeploymentConfigs(2) 100 | dcs[1].Name = dcs[0].Name 101 | dcs[0].Spec.Triggers = []oappsv1.DeploymentTriggerPolicy{ 102 | { 103 | ImageChangeParams: &oappsv1.DeploymentTriggerImageChangeParams{ 104 | Automatic: false, 105 | ContainerNames: []string{ 106 | "container1", 107 | "container2", 108 | }, 109 | From: corev1.ObjectReference{ 110 | Kind: "ImageStreamTag", 111 | Namespace: "namespace", 112 | Name: "image", 113 | }, 114 | }, 115 | }, 116 | } 117 | dcs[0].Spec.Template.Spec.Containers = []corev1.Container{ 118 | { 119 | Name: "container1", 120 | Image: "some generated value", 121 | }, 122 | } 123 | dcs[1].Spec.Triggers = []oappsv1.DeploymentTriggerPolicy{ 124 | { 125 | ImageChangeParams: &oappsv1.DeploymentTriggerImageChangeParams{ 126 | Automatic: false, 127 | ContainerNames: []string{ 128 | "container1", 129 | "container2", 130 | }, 131 | From: corev1.ObjectReference{ 132 | Kind: "ImageStreamTag", 133 | Namespace: "namespace", 134 | Name: "image", 135 | }, 136 | }, 137 | }, 138 | } 139 | dcs[1].Spec.Template.Spec.Containers = []corev1.Container{ 140 | { 141 | Name: "container1", 142 | Image: "image", 143 | }, 144 | } 145 | assert.True(t, equalDeploymentConfigs(&dcs[0], &dcs[1]), "Expected resources to be deemed equal based on DC comparator") 146 | } 147 | 148 | func TestCompareBuildConfigWebHooks(t *testing.T) { 149 | bcs := utils.GetBuildConfigs(2) 150 | bcs[1].Name = bcs[0].Name 151 | bcs[0].Spec.RunPolicy = obuildv1.BuildRunPolicySerial 152 | bcs[0].Spec.Triggers = []obuildv1.BuildTriggerPolicy{ 153 | { 154 | GitLabWebHook: &obuildv1.WebHookTrigger{ 155 | AllowEnv: false, 156 | SecretReference: &obuildv1.SecretLocalReference{Name: "dafsaf"}, 157 | }, 158 | }, 159 | } 160 | bcs[1].Spec.Triggers = []obuildv1.BuildTriggerPolicy{ 161 | { 162 | GitLabWebHook: &obuildv1.WebHookTrigger{ 163 | AllowEnv: false, 164 | SecretReference: &obuildv1.SecretLocalReference{Name: "eqwrer"}, 165 | }, 166 | }, 167 | } 168 | assert.True(t, equalBuildConfigs(&bcs[0], &bcs[1]), "Expected resources to be deemed equal based on BC comparator") 169 | } 170 | 171 | func TestCompareBuildConfigEnvVars(t *testing.T) { 172 | bcs := utils.GetBuildConfigs(2) 173 | ordered := utils.GetEnvVars(3, true) 174 | unordered := utils.GetEnvVars(3, false) 175 | bcs[1].Name = bcs[0].Name 176 | 177 | bcs[0].Spec.Strategy.SourceStrategy = &obuildv1.SourceBuildStrategy{Env: ordered} 178 | bcs[1].Spec.Strategy.SourceStrategy = &obuildv1.SourceBuildStrategy{Env: ordered} 179 | assert.True(t, equalBuildConfigs(&bcs[0], &bcs[1]), "Expected resources to be deemed equal based on BC comparator") 180 | 181 | bcs[0].Spec.Strategy.SourceStrategy = &obuildv1.SourceBuildStrategy{Env: ordered} 182 | bcs[1].Spec.Strategy.SourceStrategy = &obuildv1.SourceBuildStrategy{Env: unordered} 183 | assert.True(t, equalBuildConfigs(&bcs[0], &bcs[1]), "Expected resources to be deemed equal based on BC comparator") 184 | } 185 | 186 | func TestCompareDeployments(t *testing.T) { 187 | deployments := utils.GetDeployments(2) 188 | deployments[1].Name = deployments[0].Name 189 | deployments[1].Status = appsv1.DeploymentStatus{ 190 | ReadyReplicas: 1, 191 | } 192 | 193 | assert.False(t, reflect.DeepEqual(deployments[0], deployments[1]), "Inconsequential differences between two Deployments should make equality test fail") 194 | assert.True(t, deepEquals(&deployments[0], &deployments[1]), "Expected resources to be deemed equal") 195 | assert.True(t, equalDeployment(&deployments[0], &deployments[1]), "Expected resources to be deemed equal based on Deployment comparator") 196 | } 197 | 198 | func TestCompareDeploymentLastTriggeredImage(t *testing.T) { 199 | imageTriggersFormat := "[{\"from\":{\"kind\":\"ImageStreamTag\",\"name\":\"%s\"},\"fieldPath\":\"spec.template.spec.containers[?(@.name==\\\"%s\\\")].image\"}]" 200 | deployments := utils.GetDeployments(2) 201 | deployments[1].Name = deployments[0].Name 202 | deployments[0].Annotations = map[string]string{ 203 | imageTriggersAnnotation: fmt.Sprintf(imageTriggersFormat, "my-image", "my-container"), 204 | } 205 | deployments[1].Annotations = map[string]string{ 206 | imageTriggersAnnotation: fmt.Sprintf(imageTriggersFormat, "my-image", "my-container"), 207 | } 208 | deployments[0].Spec.Template.Spec.Containers = []corev1.Container{ 209 | {Name: "my-container", Image: "some generated value"}, 210 | } 211 | deployments[1].Spec.Template.Spec.Containers = []corev1.Container{ 212 | {Name: "my-container", Image: "quay.io/namespace/image:tag"}, 213 | } 214 | assert.True(t, equalDeployment(&deployments[0], &deployments[1]), "Expected resources to be deemed equal based on deployment comparator") 215 | } 216 | 217 | func TestCompareDeploymentGenerateValue(t *testing.T) { 218 | deployments := utils.GetDeployments(2) 219 | deployments[1].Name = deployments[0].Name 220 | deployments[0].Spec.Template.Spec.DNSPolicy = corev1.DNSClusterFirst 221 | 222 | assert.True(t, equalDeployment(&deployments[0], &deployments[1]), "Expected resources to be deemed equal based on deployment comparator") 223 | } 224 | 225 | func TestCompareSecrets(t *testing.T) { 226 | secrets := utils.GetSecrets(3) 227 | secrets[1].Name = secrets[0].Name 228 | secrets[2].Name = secrets[0].Name 229 | secrets[0].Data["password"] = []byte{'M', 'n', 'L', 'W', 'o', 'p', '3', 'P', '7', '5', 'y', 'w', 'X', 'j', 'e', 't'} 230 | secrets[0].Data["username"] = []byte{'d', 'e', 'v', 'e', 'l', 'o', 'p', 'e', 'r'} 231 | secrets[1].StringData["password"] = "MnLWop3P75ywXjet" 232 | secrets[1].StringData["username"] = "developer" 233 | secrets[2].Data["password"] = []byte{'M', 'n', 'L', 'W', 'o', 'p', '3', 'P', '7', '5', 'y', 'w', 'X', 'j', 'X', 'Y', 'e', 't'} 234 | secrets[2].Data["username"] = []byte{'d', 'e', 'v', 'o', 'p', 's'} 235 | secrets[2].StringData["password"] = "MnLWop3P75ywXjet" 236 | secrets[2].StringData["username"] = "developer" 237 | 238 | assert.False(t, reflect.DeepEqual(secrets[0], secrets[1]), "Inconsequential differences between two Secrets should make equality test fail") 239 | assert.True(t, equalSecrets(&secrets[0], &secrets[1]), "Expected resources to be deemed equal based on Secret comparator") 240 | assert.False(t, reflect.DeepEqual(secrets[0], secrets[2]), "Inconsequential differences between two Secrets should make equality test fail") 241 | assert.True(t, equalSecrets(&secrets[0], &secrets[2]), "Expected resources to be deemed equal based on Secret comparator") 242 | } 243 | 244 | func Test_mergeSecretStringDataToData(t *testing.T) { 245 | tests := []struct { 246 | name string 247 | arg *corev1.Secret 248 | want *corev1.Secret 249 | }{ 250 | { 251 | "NoStringData", 252 | &corev1.Secret{ 253 | Data: map[string][]byte{ 254 | "test": {'d', 'e', 'v', 'e', 'l', 'o', 'p', 'e', 'r'}, 255 | }, 256 | }, 257 | &corev1.Secret{ 258 | Data: map[string][]byte{ 259 | "test": {'d', 'e', 'v', 'e', 'l', 'o', 'p', 'e', 'r'}, 260 | }, 261 | }, 262 | }, 263 | { 264 | "WithStringData", 265 | &corev1.Secret{ 266 | StringData: map[string]string{ 267 | "test": "developer", 268 | }, 269 | }, 270 | &corev1.Secret{ 271 | Data: map[string][]byte{ 272 | "test": {'d', 'e', 'v', 'e', 'l', 'o', 'p', 'e', 'r'}, 273 | }, 274 | StringData: map[string]string{ 275 | "test": "developer", 276 | }, 277 | }, 278 | }, 279 | { 280 | "StringDataOverwrite", 281 | &corev1.Secret{ 282 | Data: map[string][]byte{ 283 | "test": {'"', 'Z', 'G', 'V', '2', 'Z', 'W', 'x', 'v', 'c', 'G', 'V', 'X', 'y', 'd', '"'}, 284 | }, 285 | StringData: map[string]string{ 286 | "test": "developer", 287 | }, 288 | }, 289 | &corev1.Secret{ 290 | Data: map[string][]byte{ 291 | "test": {'d', 'e', 'v', 'e', 'l', 'o', 'p', 'e', 'r'}, 292 | }, 293 | StringData: map[string]string{ 294 | "test": "developer", 295 | }, 296 | }, 297 | }, 298 | } 299 | for _, tt := range tests { 300 | t.Run(tt.name, func(t *testing.T) { 301 | if got := mergeSecretStringDataToData(tt.arg); !reflect.DeepEqual(got, tt.want) { 302 | t.Errorf("mergeSecretStringDataToData() = %v, want %v", got, tt.want) 303 | } 304 | }) 305 | } 306 | } 307 | 308 | func TestCompareUnorderedDeploymentEnvVars(t *testing.T) { 309 | deployments := utils.GetDeployments(2) 310 | deployments[1].Name = deployments[0].Name 311 | container := corev1.Container{Name: "my-container"} 312 | orderedVars := utils.GetEnvVars(3, true) 313 | unorderedVars := utils.GetEnvVars(3, false) 314 | 315 | assert.Contains(t, unorderedVars[0].Name, "3") 316 | 317 | deployments[0].Spec.Template.Spec.Containers = append(deployments[0].Spec.Template.Spec.Containers, container) 318 | deployments[1].Spec.Template.Spec.Containers = append(deployments[1].Spec.Template.Spec.Containers, container) 319 | deployments[0].Spec.Template.Spec.Containers[0].Env = orderedVars 320 | deployments[1].Spec.Template.Spec.Containers[0].Env = orderedVars 321 | 322 | assert.True(t, deepEquals(&deployments[0], &deployments[1]), "Has the same EnvVars. Expected resources to be deemed equal") 323 | assert.True(t, equalDeployment(&deployments[0], &deployments[1]), "Has the same EnvVars. Expected resources to be deemed equal based on Deployment comparator") 324 | 325 | deployments[1].Spec.Template.Spec.Containers[0].Env = unorderedVars 326 | 327 | assert.True(t, deepEquals(&deployments[0], &deployments[1]), "Has the same EnvVars, unordered. Expected resources to be deemed equal") 328 | assert.True(t, equalDeployment(&deployments[0], &deployments[1]), "Has the same EnvVars, unordered. Expected resources to be deemed equal based on Deployment comparator") 329 | } 330 | 331 | func TestCompareUnorderedDeploymentConfigEnvVars(t *testing.T) { 332 | deployments := utils.GetDeploymentConfigs(2) 333 | deployments[1].Name = deployments[0].Name 334 | container := corev1.Container{Name: "my-container"} 335 | orderedVars := utils.GetEnvVars(3, true) 336 | unorderedVars := utils.GetEnvVars(3, false) 337 | deployments[0].Spec.Template.Spec.Containers = append(deployments[0].Spec.Template.Spec.Containers, container) 338 | deployments[1].Spec.Template.Spec.Containers = append(deployments[1].Spec.Template.Spec.Containers, container) 339 | deployments[0].Spec.Template.Spec.Containers[0].Env = orderedVars 340 | deployments[1].Spec.Template.Spec.Containers[0].Env = orderedVars 341 | 342 | assert.True(t, deepEquals(&deployments[0], &deployments[1]), "Has the same EnvVars. Expected resources to be deemed equal") 343 | assert.True(t, equalDeploymentConfigs(&deployments[0], &deployments[1]), "Has the same EnvVars. Expected resources to be deemed equal based on DeploymentConfig comparator") 344 | 345 | deployments[1].Spec.Template.Spec.Containers[0].Env = unorderedVars 346 | 347 | assert.True(t, deepEquals(&deployments[0], &deployments[1]), "Has the same EnvVars, unordered. Expected resources to be deemed equal") 348 | assert.True(t, equalDeploymentConfigs(&deployments[0], &deployments[1]), "Has the same EnvVars, unordered. Expected resources to be deemed equal based on DeploymentConfig comparator") 349 | } 350 | -------------------------------------------------------------------------------- /pkg/resource/compare/map.go: -------------------------------------------------------------------------------- 1 | package compare 2 | 3 | import ( 4 | "reflect" 5 | 6 | ctrl "sigs.k8s.io/controller-runtime" 7 | "sigs.k8s.io/controller-runtime/pkg/client" 8 | ) 9 | 10 | var logger = ctrl.Log.WithName("comparator") 11 | 12 | type MapComparator struct { 13 | Comparator ResourceComparator 14 | } 15 | 16 | func NewMapComparator() MapComparator { 17 | return MapComparator{ 18 | Comparator: DefaultComparator(), 19 | } 20 | } 21 | 22 | func (this *MapComparator) Compare(deployed map[reflect.Type][]client.Object, requested map[reflect.Type][]client.Object) map[reflect.Type]ResourceDelta { 23 | delta := make(map[reflect.Type]ResourceDelta) 24 | for deployedType, deployedArray := range deployed { 25 | requestedArray := requested[deployedType] 26 | delta[deployedType] = this.Comparator.CompareArrays(deployedArray, requestedArray) 27 | } 28 | for requestedType, requestedArray := range requested { 29 | if _, ok := deployed[requestedType]; !ok { 30 | //Item type in request does not exist in deployed set, needs to be added: 31 | delta[requestedType] = ResourceDelta{Added: requestedArray} 32 | } 33 | } 34 | return delta 35 | } 36 | -------------------------------------------------------------------------------- /pkg/resource/compare/test/external_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/RHsyseng/operator-utils/pkg/resource/compare" 8 | "github.com/RHsyseng/operator-utils/pkg/resource/test" 9 | oappsv1 "github.com/openshift/api/apps/v1" 10 | "github.com/stretchr/testify/assert" 11 | appsv1 "k8s.io/api/apps/v1" 12 | corev1 "k8s.io/api/core/v1" 13 | "k8s.io/apimachinery/pkg/api/resource" 14 | "sigs.k8s.io/controller-runtime/pkg/client" 15 | ) 16 | 17 | func TestCompareServices(t *testing.T) { 18 | svcs := test.GetServices(2) 19 | svcs[0].Status = corev1.ServiceStatus{ 20 | LoadBalancer: corev1.LoadBalancerStatus{ 21 | Ingress: []corev1.LoadBalancerIngress{ 22 | { 23 | IP: "127.0.0.1", 24 | Hostname: "localhost", 25 | }, 26 | }, 27 | }, 28 | } 29 | svcs[1].Name = svcs[0].Name 30 | 31 | assert.False(t, reflect.DeepEqual(svcs[0], svcs[1]), "Inconsequential differences between two services should make equality test fail") 32 | assert.True(t, compare.SimpleComparator().Compare(&svcs[0], &svcs[1]), "Expected resources to be deemed equal") 33 | assert.True(t, compare.DefaultComparator().Compare(&svcs[0], &svcs[1]), "Expected resources to be deemed equal based on service comparator") 34 | } 35 | 36 | func TestCompareDeploymentConfigs(t *testing.T) { 37 | dcs := test.GetDeploymentConfigs(2) 38 | dcs[1].Name = dcs[0].Name 39 | dcs[1].Status = oappsv1.DeploymentConfigStatus{ 40 | ReadyReplicas: 1, 41 | } 42 | 43 | assert.False(t, reflect.DeepEqual(dcs[0], dcs[1]), "Inconsequential differences between two DCs should make equality test fail") 44 | assert.True(t, compare.SimpleComparator().Compare(&dcs[0], &dcs[1]), "Expected resources to be deemed equal") 45 | assert.True(t, compare.DefaultComparator().Compare(&dcs[0], &dcs[1]), "Expected resources to be deemed equal based on DC comparator") 46 | } 47 | 48 | func TestCompareCombined(t *testing.T) { 49 | dcs := test.GetDeploymentConfigs(6) 50 | dc1a := dcs[0] 51 | dc1b := dcs[1] 52 | dc2a := dcs[2] 53 | dc2b := dcs[3] 54 | dc3a := dcs[4] 55 | dc4b := dcs[5] 56 | dc1a.Name = "dc1" 57 | dc1b.Name = "dc1" 58 | dc2a.Name = "dc2" 59 | dc2b.Name = "dc2" 60 | dc3a.Name = "dc3" 61 | dc4b.Name = "dc4" 62 | dc1b.Status = oappsv1.DeploymentConfigStatus{ 63 | Replicas: 2, 64 | } 65 | dc2b.Spec.Replicas = 2 66 | 67 | svcs := test.GetServices(6) 68 | service1a := svcs[0] 69 | service1b := svcs[1] 70 | service2a := svcs[2] 71 | service2b := svcs[3] 72 | service3a := svcs[4] 73 | service4b := svcs[5] 74 | service1a.Name = "service1" 75 | service1b.Name = "service1" 76 | service2a.Name = "service2" 77 | service2b.Name = "service2" 78 | service3a.Name = "service3" 79 | service4b.Name = "service4" 80 | service1b.Status = corev1.ServiceStatus{ 81 | LoadBalancer: corev1.LoadBalancerStatus{ 82 | Ingress: []corev1.LoadBalancerIngress{ 83 | { 84 | IP: "127.0.0.1", 85 | Hostname: "localhost", 86 | }, 87 | }, 88 | }, 89 | } 90 | service2b.Spec = corev1.ServiceSpec{ 91 | ClusterIP: "127.0.0.1", 92 | } 93 | 94 | serviceType := reflect.TypeOf(corev1.Service{}) 95 | dcType := reflect.TypeOf(oappsv1.DeploymentConfig{}) 96 | deployed := map[reflect.Type][]client.Object{ 97 | dcType: {&dc1a, &dc2a, &dc3a}, 98 | serviceType: {&service1a, &service2a, &service3a}, 99 | } 100 | requested := map[reflect.Type][]client.Object{ 101 | dcType: {&dc1b, &dc2b, &dc4b}, 102 | serviceType: {&service1b, &service2b, &service4b}, 103 | } 104 | 105 | mapComparator := compare.NewMapComparator() 106 | deltaMap := mapComparator.Compare(deployed, requested) 107 | 108 | assert.Len(t, deltaMap[serviceType].Added, 1, "Expected 1 added service") 109 | assert.Equal(t, deltaMap[serviceType].Added[0].GetName(), "service4", "Expected added service called service4") 110 | assert.Len(t, deltaMap[serviceType].Updated, 1, "Expected 1 updated service") 111 | assert.Equal(t, deltaMap[serviceType].Updated[0].GetName(), "service2", "Expected added service called service2") 112 | assert.Len(t, deltaMap[serviceType].Removed, 1, "Expected 1 removed service") 113 | assert.Equal(t, deltaMap[serviceType].Removed[0].GetName(), "service3", "Expected added service called service3") 114 | assert.Len(t, deltaMap[dcType].Added, 1, "Expected 1 added dc") 115 | assert.Equal(t, deltaMap[dcType].Added[0].GetName(), "dc4", "Expected added dc called dc4") 116 | assert.Len(t, deltaMap[dcType].Updated, 1, "Expected 1 updated dc") 117 | assert.Equal(t, deltaMap[dcType].Updated[0].GetName(), "dc2", "Expected updated dc called dc2") 118 | assert.Len(t, deltaMap[dcType].Removed, 1, "Expected 1 removed dc") 119 | assert.Equal(t, deltaMap[dcType].Removed[0].GetName(), "dc3", "Expected removed dc called dc3") 120 | } 121 | 122 | func TestCompareDeployment(t *testing.T) { 123 | dep1 := appsv1.Deployment{ 124 | Spec: appsv1.DeploymentSpec{ 125 | Template: corev1.PodTemplateSpec{ 126 | Spec: corev1.PodSpec{ 127 | Containers: []corev1.Container{ 128 | { 129 | Resources: corev1.ResourceRequirements{ 130 | Limits: corev1.ResourceList{ 131 | corev1.ResourceCPU: *resource.NewScaledQuantity(1000000, resource.Milli), 132 | }, 133 | }, 134 | }, 135 | }, 136 | }, 137 | }, 138 | }, 139 | } 140 | dep2 := appsv1.Deployment{ 141 | Spec: appsv1.DeploymentSpec{ 142 | Template: corev1.PodTemplateSpec{ 143 | Spec: corev1.PodSpec{ 144 | Containers: []corev1.Container{ 145 | { 146 | Resources: corev1.ResourceRequirements{ 147 | Limits: corev1.ResourceList{ 148 | corev1.ResourceCPU: *resource.NewScaledQuantity(1, resource.Kilo), 149 | }, 150 | }, 151 | }, 152 | }, 153 | }, 154 | }, 155 | }, 156 | } 157 | 158 | assert.True(t, compare.Equals(dep1, dep2)) 159 | } 160 | -------------------------------------------------------------------------------- /pkg/resource/compare/test/utils_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/RHsyseng/operator-utils/pkg/resource/compare" 5 | oappsv1 "github.com/openshift/api/apps/v1" 6 | routev1 "github.com/openshift/api/route/v1" 7 | "github.com/stretchr/testify/assert" 8 | corev1 "k8s.io/api/core/v1" 9 | "reflect" 10 | "testing" 11 | ) 12 | 13 | func TestEmptyArray(t *testing.T) { 14 | builder := compare.NewMapBuilder() 15 | assert.Empty(t, builder.ResourceMap(), "Expected empty map") 16 | } 17 | 18 | func TestMapBuilder(t *testing.T) { 19 | resMap := compare.NewMapBuilder().Add(&routev1.Route{}, &routev1.Route{}, &corev1.Service{}).Add(&oappsv1.DeploymentConfig{}).ResourceMap() 20 | assert.Len(t, resMap, 3, "Expect map to have 3 entries") 21 | assert.Len(t, resMap[reflect.TypeOf(routev1.Route{})], 2, "Expect map to have 2 routes") 22 | assert.Len(t, resMap[reflect.TypeOf(corev1.Service{})], 1, "Expect map to have 1 service") 23 | assert.Len(t, resMap[reflect.TypeOf(oappsv1.DeploymentConfig{})], 1, "Expect map to have 1 deployment config") 24 | } 25 | 26 | func TestNilResources(t *testing.T) { 27 | mapBuilder := compare.NewMapBuilder() 28 | mapBuilder.Add(nil) 29 | assert.Len(t, mapBuilder.ResourceMap(), 0, "Expect map to have zero entries") 30 | dcPtr := &oappsv1.DeploymentConfig{} 31 | dcPtr = nil 32 | mapBuilder.Add(dcPtr) 33 | assert.Len(t, mapBuilder.ResourceMap(), 0, "Expect map to have zero entries") 34 | } 35 | -------------------------------------------------------------------------------- /pkg/resource/compare/types.go: -------------------------------------------------------------------------------- 1 | package compare 2 | 3 | import ( 4 | "reflect" 5 | "sigs.k8s.io/controller-runtime/pkg/client" 6 | ) 7 | 8 | type ResourceDelta struct { 9 | Added []client.Object 10 | Updated []client.Object 11 | Removed []client.Object 12 | } 13 | 14 | func (delta *ResourceDelta) HasChanges() bool { 15 | if len(delta.Added) > 0 { 16 | return true 17 | } 18 | if len(delta.Updated) > 0 { 19 | return true 20 | } 21 | if len(delta.Removed) > 0 { 22 | return true 23 | } 24 | return false 25 | } 26 | 27 | type ResourceComparator interface { 28 | SetDefaultComparator(compFunc func(deployed client.Object, requested client.Object) bool) 29 | GetDefaultComparator() func(deployed client.Object, requested client.Object) bool 30 | SetComparator(resourceType reflect.Type, compFunc func(deployed client.Object, requested client.Object) bool) 31 | GetComparator(resourceType reflect.Type) func(deployed client.Object, requested client.Object) bool 32 | Compare(deployed client.Object, requested client.Object) bool 33 | CompareArrays(deployed []client.Object, requested []client.Object) ResourceDelta 34 | } 35 | 36 | func DefaultComparator() ResourceComparator { 37 | return &resourceComparator{ 38 | deepEquals, 39 | defaultMap(), 40 | } 41 | } 42 | 43 | func SimpleComparator() ResourceComparator { 44 | return &resourceComparator{ 45 | deepEquals, 46 | make(map[reflect.Type]func(client.Object, client.Object) bool), 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pkg/resource/compare/utils.go: -------------------------------------------------------------------------------- 1 | package compare 2 | 3 | import ( 4 | "reflect" 5 | "sigs.k8s.io/controller-runtime/pkg/client" 6 | ) 7 | 8 | type mapBuilder struct { 9 | resourceMap map[reflect.Type][]client.Object 10 | } 11 | 12 | func NewMapBuilder() *mapBuilder { 13 | this := &mapBuilder{resourceMap: make(map[reflect.Type][]client.Object)} 14 | return this 15 | } 16 | 17 | func (this *mapBuilder) ResourceMap() map[reflect.Type][]client.Object { 18 | return this.resourceMap 19 | } 20 | 21 | func (this *mapBuilder) Add(resources ...client.Object) *mapBuilder { 22 | for index := range resources { 23 | if resources[index] == nil || reflect.ValueOf(resources[index]).IsNil() { 24 | continue 25 | } 26 | resourceType := reflect.ValueOf(resources[index]).Elem().Type() 27 | this.resourceMap[resourceType] = append(this.resourceMap[resourceType], resources[index]) 28 | } 29 | return this 30 | } 31 | -------------------------------------------------------------------------------- /pkg/resource/detector/README.md: -------------------------------------------------------------------------------- 1 | ### Example usage: 2 | 3 | Setting up the detector: 4 | ```go 5 | //you would most likely have this code in your main.go 6 | dc, err := discovery.NewDiscoveryClientForConfig(mgr.GetConfig()) 7 | if err != nil { 8 | panic("Could not create discovery client") 9 | } 10 | 11 | d, err := detector.NewAutoDetect(dc) 12 | if err != nil { 13 | panic("error creating autodetector: " + err.Error()) 14 | } 15 | 16 | d.Start(5 * time.Second) //scan for new CRDs every 5 seconds 17 | ``` 18 | 19 | Triggering an action when a particular CRD shows up: 20 | ```go 21 | // and pass this detector instance to the `add` function of your operator's controller, where you could run: 22 | d.AddCRDTrigger(&package.CRD{ 23 | TypeMeta: metav1.TypeMeta{ 24 | Kind: package.CrdKind, 25 | APIVersion: package.SchemeGroupVersion.String(), 26 | }, 27 | }, func(crd runtime.Object) { 28 | // Do actions now that the package.CRD exists in the API, e.g begin watching it: 29 | c.Watch(&source.Kind{Type: &package.CRD{}}, &EnqueueForObject{}) 30 | }) 31 | ``` 32 | 33 | Triggering an action when any of multiple CRDs show up: 34 | ```go 35 | // and pass this detector instance to the `add` function of your operator's controller, where you could run: 36 | d.AddCRDsTrigger([]runtime.Object{ 37 | &package.CRD{ 38 | TypeMeta: metav1.TypeMeta{ 39 | Kind: package.CrdKind, 40 | APIVersion: package.SchemeGroupVersion.String(), 41 | }, 42 | }, 43 | &package.OtherCrd{ 44 | TypeMeta: metav1.TypeMeta{ 45 | Kind: package.OtherCrdKind: 46 | APIVersion: package.SchemeGroupVersion.String(), 47 | }, 48 | }, 49 | }, func(crd runtime.Object) { 50 | // Do actions now that the package.CRD exists in the API, e.g begin watching it: 51 | c.Watch(&source.Kind{Type: crd}, &EnqueueForObject{}) 52 | }) 53 | ``` -------------------------------------------------------------------------------- /pkg/resource/detector/detector.go: -------------------------------------------------------------------------------- 1 | package detector 2 | 3 | import ( 4 | "time" 5 | 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "k8s.io/apimachinery/pkg/runtime" 8 | "k8s.io/client-go/discovery" 9 | ) 10 | 11 | // Detector represents a procedure that runs in the background, periodically auto-detecting features 12 | type Detector struct { 13 | dc discovery.DiscoveryInterface 14 | ticker *time.Ticker 15 | crds map[runtime.Object]trigger 16 | } 17 | 18 | type trigger func(runtime.Object) 19 | 20 | // New creates a new auto-detect runner 21 | func NewAutoDetect(dc discovery.DiscoveryInterface) (*Detector, error) { 22 | return &Detector{dc: dc, crds: map[runtime.Object]trigger{}}, nil 23 | } 24 | 25 | // AddCRDTrigger to run the trigger function, 26 | // the first time that the background scanner discovers that the CRD type specified exists 27 | func (d *Detector) AddCRDTrigger(crd runtime.Object, trigger trigger) { 28 | d.crds[crd] = trigger 29 | } 30 | 31 | // AddCRDsTrigger to run the trigger function, 32 | // the first time that the background scanner discovers that each of the CRD types specified exists 33 | func (d *Detector) AddCRDsTrigger(crds []runtime.Object, trigger trigger) { 34 | for _, crd := range crds { 35 | d.AddCRDTrigger(crd, trigger) 36 | } 37 | } 38 | 39 | // AddCRDsWithTriggers to run the associated trigger function for the particular CRD, 40 | // the first time that the background scanner discovers that the CRD type specified exists 41 | func (d *Detector) AddCRDsWithTriggers(crdsTriggers map[runtime.Object]trigger) { 42 | for crd, trigger := range crdsTriggers { 43 | d.AddCRDTrigger(crd, trigger) 44 | } 45 | } 46 | 47 | // Start initializes the auto-detection process that runs in the background 48 | func (d *Detector) Start(interval time.Duration) { 49 | go func() { 50 | d.autoDetectCapabilities() 51 | d.ticker = time.NewTicker(interval) 52 | for range d.ticker.C { 53 | d.autoDetectCapabilities() 54 | } 55 | }() 56 | } 57 | 58 | // Stop causes the background process to stop auto detecting capabilities 59 | func (d *Detector) Stop() { 60 | d.ticker.Stop() 61 | } 62 | 63 | func (d *Detector) autoDetectCapabilities() { 64 | for crd, trigger := range d.crds { 65 | crdGVK := crd.GetObjectKind().GroupVersionKind() 66 | apiLists, err := d.dc.ServerResourcesForGroupVersion(crdGVK.GroupVersion().String()) 67 | if err != nil { 68 | return 69 | } 70 | resourceExists := d.resourceExists(apiLists, crdGVK.Kind) 71 | if resourceExists { 72 | stateManager := GetStateManager() 73 | if stateManager.GetState(crdGVK.Kind) != true { 74 | stateManager.SetState(crdGVK.Kind, true) 75 | trigger(crd) 76 | } 77 | } 78 | } 79 | } 80 | 81 | func (d *Detector) resourceExists(apiList *metav1.APIResourceList, kind string) bool { 82 | for _, r := range apiList.APIResources { 83 | if r.Kind == kind { 84 | return true 85 | } 86 | } 87 | return false 88 | } 89 | -------------------------------------------------------------------------------- /pkg/resource/detector/detector_test.go: -------------------------------------------------------------------------------- 1 | package detector 2 | 3 | import ( 4 | appsv1 "k8s.io/api/apps/v1" 5 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | "k8s.io/apimachinery/pkg/runtime" 7 | discoveryFake "k8s.io/client-go/discovery/fake" 8 | k8sTesting "k8s.io/client-go/testing" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestDetectorDetects(t *testing.T) { 14 | crdDiscovered := false 15 | dc := &discoveryFake.FakeDiscovery{ 16 | Fake: &k8sTesting.Fake{}, 17 | FakedServerVersion: nil, 18 | } 19 | 20 | d, err := NewAutoDetect(dc) 21 | if err != nil { 22 | t.Fatalf("expected no errors, got: %s", err.Error()) 23 | } 24 | 25 | // run very frequently, for faster tests 26 | d.Start(10 * time.Nanosecond) 27 | d.AddCRDTrigger(&appsv1.Deployment{ 28 | TypeMeta: metav1.TypeMeta{ 29 | Kind: "deployment", 30 | APIVersion: appsv1.SchemeGroupVersion.String(), 31 | }, 32 | }, func(crd runtime.Object) { 33 | crdDiscovered = true 34 | 35 | }) 36 | 37 | //wait a few intervals 38 | time.Sleep(10 * time.Millisecond) 39 | 40 | if crdDiscovered { 41 | t.Fatalf("CRD Discovered too early") 42 | } 43 | 44 | dc.Resources = []*metav1.APIResourceList{ 45 | { 46 | TypeMeta: metav1.TypeMeta{}, 47 | GroupVersion: "apps/v1", 48 | APIResources: []metav1.APIResource{{Kind: "deployment"}}, 49 | }, 50 | } 51 | 52 | time.Sleep(10 * time.Millisecond) 53 | if !crdDiscovered { 54 | t.Fatalf("CRD not discovered correctly") 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /pkg/resource/detector/stateManager.go: -------------------------------------------------------------------------------- 1 | package detector 2 | 3 | import "sync" 4 | 5 | const ( 6 | RealmLabelSelectorsKey = "realmLabelSelectors" 7 | ) 8 | 9 | type StateManager struct { 10 | *sync.Mutex 11 | state map[string]interface{} 12 | } 13 | 14 | var singleton *StateManager 15 | var once sync.Once 16 | 17 | func GetStateManager() *StateManager { 18 | once.Do(func() { 19 | singleton = &StateManager{Mutex: &sync.Mutex{}} 20 | singleton.state = make(map[string]interface{}) 21 | }) 22 | return singleton 23 | } 24 | 25 | func (sm *StateManager) GetState(key string) interface{} { 26 | sm.Lock() 27 | defer sm.Unlock() 28 | return sm.state[key] 29 | } 30 | 31 | func (sm *StateManager) SetState(key string, value interface{}) { 32 | sm.Lock() 33 | defer sm.Unlock() 34 | sm.state[key] = value 35 | } 36 | 37 | func (sm *StateManager) Clear() { 38 | sm.Lock() 39 | defer sm.Unlock() 40 | sm.state = make(map[string]interface{}) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/resource/detector/stateManager_test.go: -------------------------------------------------------------------------------- 1 | package detector 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestStateManager_Test_(t *testing.T) { 8 | stateManager := GetStateManager() 9 | stateManagerTwo := GetStateManager() 10 | 11 | stateManager.SetState("Test", "string") 12 | 13 | if stateManager.GetState("NotSet") != nil { 14 | t.Fatalf("Expected nil, got '%s'", stateManager.GetState("NotSet")) 15 | } 16 | 17 | if stateManager.GetState("Test") != "string" { 18 | t.Fatalf("Expected 'string' got '%s'", stateManager.GetState("Test")) 19 | } 20 | 21 | if stateManager != stateManagerTwo { 22 | t.Fatalf("Expected objects to be equal") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/resource/read/reader.go: -------------------------------------------------------------------------------- 1 | package read 2 | 3 | import ( 4 | "context" 5 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | "k8s.io/apimachinery/pkg/types" 7 | "reflect" 8 | "sigs.k8s.io/controller-runtime/pkg/client" 9 | ) 10 | 11 | type resourceReader struct { 12 | reader client.Reader 13 | namespace string 14 | ownerObject metav1.Object 15 | } 16 | 17 | // New creates a resourceReader object that can be used to load/list kubernetes resources 18 | // the provided reader object will be used for the underlying operations 19 | func New(reader client.Reader) *resourceReader { 20 | return &resourceReader{reader: reader} 21 | } 22 | 23 | // WithNamespace filters list operations to the provided namespace 24 | func (this *resourceReader) WithNamespace(namespace string) *resourceReader { 25 | this.namespace = namespace 26 | return this 27 | } 28 | 29 | // WithOwnerObject filters list operations to items that have ownerObject as an owner reference 30 | func (this *resourceReader) WithOwnerObject(ownerObject metav1.Object) *resourceReader { 31 | this.ownerObject = ownerObject 32 | return this 33 | } 34 | 35 | // List returns a list of Kubernetes resources based on provided List object and configuration 36 | // any error from underlying calls is directly returned as well 37 | func (this *resourceReader) List(listObject client.ObjectList) ([]client.Object, error) { 38 | var resources []client.Object 39 | err := this.reader.List(context.TODO(), listObject, &client.ListOptions{Namespace: this.namespace}) 40 | if err != nil { 41 | return nil, err 42 | } 43 | itemsValue := reflect.Indirect(reflect.ValueOf(listObject)).FieldByName("Items") 44 | for index := 0; index < itemsValue.Len(); index++ { 45 | item := addr(itemsValue.Index(index)).Interface().(client.Object) 46 | if this.ownerObject == nil || isOwner(this.ownerObject, item) { 47 | resources = append(resources, item) 48 | } 49 | } 50 | return resources, nil 51 | } 52 | 53 | func addr(v reflect.Value) reflect.Value { 54 | if v.Kind() == reflect.Ptr { 55 | return v 56 | } 57 | 58 | return v.Addr() 59 | } 60 | 61 | func isOwner(owner metav1.Object, res client.Object) bool { 62 | for _, ownerRef := range res.GetOwnerReferences() { 63 | if ownerRef.UID == owner.GetUID() { 64 | return true 65 | } 66 | } 67 | return false 68 | } 69 | 70 | // ListAll returns a map of Kubernetes resources organized by type, based on provided List objects and configuration 71 | // any error from underlying calls is directly returned as well 72 | func (this *resourceReader) ListAll(listObjects ...client.ObjectList) (map[reflect.Type][]client.Object, error) { 73 | objectMap := make(map[reflect.Type][]client.Object) 74 | for _, listObject := range listObjects { 75 | resources, err := this.List(listObject) 76 | if err != nil { 77 | return nil, err 78 | } 79 | if len(resources) > 0 { 80 | itemType := reflect.ValueOf(resources[0]).Elem().Type() 81 | objectMap[itemType] = resources 82 | } 83 | } 84 | return objectMap, nil 85 | } 86 | 87 | // Load returns an object of the specified type with the given name, in the previously configured namespace 88 | // any error from the underlying call, including a not-found error, is directly returned as well 89 | func (this *resourceReader) Load(resourceType reflect.Type, name string) (client.Object, error) { 90 | deployed := reflect.New(resourceType).Interface().(client.Object) 91 | err := this.reader.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: this.namespace}, deployed) 92 | return deployed, err 93 | } 94 | -------------------------------------------------------------------------------- /pkg/resource/read/reader_test.go: -------------------------------------------------------------------------------- 1 | package read 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "testing" 8 | 9 | monv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" 10 | "github.com/stretchr/testify/assert" 11 | corev1 "k8s.io/api/core/v1" 12 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/apimachinery/pkg/runtime" 14 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 15 | ) 16 | 17 | var namespace = "ns" 18 | 19 | func TestListObjects(t *testing.T) { 20 | scheme := runtime.NewScheme() 21 | err := corev1.SchemeBuilder.AddToScheme(scheme) 22 | assert.Nil(t, err, "Expect no errors building scheme") 23 | err = monv1.SchemeBuilder.AddToScheme(scheme) 24 | assert.Nil(t, err, "Expect no errors building scheme") 25 | client := fake.NewFakeClientWithScheme(scheme) 26 | services := getServices(2) 27 | for index := range services { 28 | services[index].ResourceVersion = "" 29 | assert.Nil(t, client.Create(context.TODO(), &services[index]), "Expect no errors mock creating objects") 30 | } 31 | pods := getPods(3) 32 | for index := range pods { 33 | pods[index].ResourceVersion = "" 34 | assert.Nil(t, client.Create(context.TODO(), &pods[index]), "Expect no errors mock creating objects") 35 | } 36 | serviceMonitors := getServiceMonitors(2) 37 | for index := range serviceMonitors { 38 | serviceMonitors[index].ResourceVersion = "" 39 | assert.Nil(t, client.Create(context.TODO(), &serviceMonitors[index]), "Expect no errors mock creating objects") 40 | } 41 | 42 | reader := New(client).WithNamespace(namespace) 43 | objectMap, err := reader.ListAll(&corev1.ServiceList{}, &corev1.PodList{}, &monv1.ServiceMonitorList{}) 44 | assert.Nil(t, err, "Expect no errors listing objects") 45 | assert.Len(t, objectMap, 3, "Expect two object types found") 46 | 47 | listedServices := objectMap[reflect.TypeOf(corev1.Service{})] 48 | assert.Len(t, listedServices, 2, "Expect to find 2 services") 49 | expectedServices := getServices(2) 50 | assert.Equal(t, &expectedServices[0], listedServices[0]) 51 | assert.Equal(t, &expectedServices[1], listedServices[1]) 52 | 53 | listedPods := objectMap[reflect.TypeOf(corev1.Pod{})] 54 | assert.Len(t, listedPods, 3, "Expect to find 3 pods") 55 | expectedPods := getPods(3) 56 | assert.Equal(t, &expectedPods[0], listedPods[0]) 57 | assert.Equal(t, &expectedPods[1], listedPods[1]) 58 | assert.Equal(t, &expectedPods[2], listedPods[2]) 59 | 60 | listedServiceMonitors := objectMap[reflect.TypeOf(monv1.ServiceMonitor{})] 61 | assert.Len(t, listedServiceMonitors, 2, "Expect to find 2 servicemonitors") 62 | expectedServiceMonitors := getServiceMonitors(2) 63 | assert.Equal(t, &expectedServiceMonitors[0], listedServiceMonitors[0]) 64 | assert.Equal(t, &expectedServiceMonitors[1], listedServiceMonitors[1]) 65 | } 66 | 67 | func TestLoadObject(t *testing.T) { 68 | scheme := runtime.NewScheme() 69 | err := corev1.SchemeBuilder.AddToScheme(scheme) 70 | assert.Nil(t, err, "Expect no errors building scheme") 71 | client := fake.NewFakeClientWithScheme(scheme) 72 | service := getServices(1)[0] 73 | service.ResourceVersion = "" 74 | assert.Nil(t, client.Create(context.TODO(), &service), "Expect no errors mock creating object") 75 | 76 | reader := New(client).WithNamespace(namespace) 77 | found, err := reader.Load(reflect.TypeOf(service), service.Name) 78 | assert.Equal(t, &service, found) 79 | } 80 | 81 | func getServices(count int) []corev1.Service { 82 | services := make([]corev1.Service, count) 83 | for index := range services { 84 | services[index] = corev1.Service{ 85 | TypeMeta: v1.TypeMeta{ 86 | Kind: "Service", 87 | APIVersion: "v1", 88 | }, 89 | ObjectMeta: v1.ObjectMeta{ 90 | Name: fmt.Sprintf("service-%d", index+1), 91 | Namespace: namespace, 92 | ResourceVersion: "1", 93 | }, 94 | } 95 | } 96 | return services 97 | } 98 | 99 | func getPods(count int) []corev1.Pod { 100 | pods := make([]corev1.Pod, count) 101 | for index := range pods { 102 | pods[index] = corev1.Pod{ 103 | ObjectMeta: v1.ObjectMeta{ 104 | Name: fmt.Sprintf("pod-%d", index+1), 105 | Namespace: namespace, 106 | ResourceVersion: "1", 107 | }, 108 | } 109 | } 110 | return pods 111 | } 112 | 113 | func getServiceMonitors(count int) []monv1.ServiceMonitor { 114 | servicemonitors := make([]monv1.ServiceMonitor, count) 115 | for index := range servicemonitors { 116 | servicemonitors[index] = monv1.ServiceMonitor{ 117 | ObjectMeta: v1.ObjectMeta{ 118 | Name: fmt.Sprintf("servicemonitor-%d", index+1), 119 | Namespace: namespace, 120 | ResourceVersion: "1", 121 | }, 122 | } 123 | } 124 | return servicemonitors 125 | } 126 | -------------------------------------------------------------------------------- /pkg/resource/test/utils.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | oappsv1 "github.com/openshift/api/apps/v1" 6 | buildv1 "github.com/openshift/api/build/v1" 7 | routev1 "github.com/openshift/api/route/v1" 8 | appsv1 "k8s.io/api/apps/v1" 9 | corev1 "k8s.io/api/core/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | ) 12 | 13 | func GetRoutes(count int) []routev1.Route { 14 | var slice []routev1.Route 15 | for i := 0; i < count; i++ { 16 | rte := routev1.Route{ 17 | TypeMeta: metav1.TypeMeta{}, 18 | Spec: routev1.RouteSpec{}, 19 | Status: routev1.RouteStatus{}, 20 | } 21 | rte.Name = fmt.Sprintf("%s%d", "rte", (i + 1)) 22 | slice = append(slice, rte) 23 | } 24 | return slice 25 | } 26 | 27 | func GetServices(count int) []corev1.Service { 28 | var slice []corev1.Service 29 | for i := 0; i < count; i++ { 30 | svc := corev1.Service{ 31 | TypeMeta: metav1.TypeMeta{}, 32 | Spec: corev1.ServiceSpec{}, 33 | Status: corev1.ServiceStatus{}, 34 | } 35 | svc.Name = fmt.Sprintf("%s%d", "svc", (i + 1)) 36 | slice = append(slice, svc) 37 | } 38 | return slice 39 | } 40 | 41 | func GetDeploymentConfigs(count int) []oappsv1.DeploymentConfig { 42 | var slice []oappsv1.DeploymentConfig 43 | for i := 0; i < count; i++ { 44 | dc := oappsv1.DeploymentConfig{ 45 | TypeMeta: metav1.TypeMeta{}, 46 | ObjectMeta: metav1.ObjectMeta{}, 47 | Spec: oappsv1.DeploymentConfigSpec{ 48 | Template: &corev1.PodTemplateSpec{}, 49 | }, 50 | Status: oappsv1.DeploymentConfigStatus{ 51 | ReadyReplicas: 0, 52 | }, 53 | } 54 | dc.Name = fmt.Sprintf("%s%d", "dc", (i + 1)) 55 | slice = append(slice, dc) 56 | } 57 | return slice 58 | } 59 | 60 | func GetBuildConfigs(count int) []buildv1.BuildConfig { 61 | var slice []buildv1.BuildConfig 62 | for i := 0; i < count; i++ { 63 | bc := buildv1.BuildConfig{} 64 | bc.Name = fmt.Sprintf("%s%d", "bc", (i + 1)) 65 | slice = append(slice, bc) 66 | } 67 | return slice 68 | } 69 | 70 | func GetDeployments(count int) []appsv1.Deployment { 71 | var slice []appsv1.Deployment 72 | for i := 0; i < count; i++ { 73 | dc := appsv1.Deployment{ 74 | TypeMeta: metav1.TypeMeta{}, 75 | ObjectMeta: metav1.ObjectMeta{}, 76 | Spec: appsv1.DeploymentSpec{ 77 | Template: corev1.PodTemplateSpec{}, 78 | }, 79 | Status: appsv1.DeploymentStatus{ 80 | ReadyReplicas: 0, 81 | }, 82 | } 83 | dc.Name = fmt.Sprintf("%s%d", "deployment", i+1) 84 | slice = append(slice, dc) 85 | } 86 | return slice 87 | } 88 | 89 | func GetSecrets(count int) []corev1.Secret { 90 | var slice []corev1.Secret 91 | for i := 0; i < count; i++ { 92 | secret := corev1.Secret{ 93 | TypeMeta: metav1.TypeMeta{}, 94 | ObjectMeta: metav1.ObjectMeta{}, 95 | Data: map[string][]byte{}, 96 | StringData: map[string]string{}, 97 | } 98 | secret.Name = fmt.Sprintf("%s%d", "secret", (i + 1)) 99 | slice = append(slice, secret) 100 | } 101 | return slice 102 | } 103 | 104 | func GetEnvVars(count int, ordered bool) []corev1.EnvVar { 105 | var slice []corev1.EnvVar 106 | suffix := 0 107 | for i := 0; i < count; i++ { 108 | if ordered { 109 | suffix = i + 1 110 | } else { 111 | suffix = count - i 112 | } 113 | env := corev1.EnvVar{Name: fmt.Sprintf("VAR%d", suffix), Value: fmt.Sprintf("value_%d", suffix)} 114 | slice = append(slice, env) 115 | } 116 | 117 | return slice 118 | } 119 | -------------------------------------------------------------------------------- /pkg/resource/test/utils_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestDeploymentConfigGeneration(t *testing.T) { 10 | dcs := GetDeploymentConfigs(3) 11 | assert.Len(t, dcs, 3, "Expected 3 DCs in the array") 12 | for i := 0; i < len(dcs); i++ { 13 | assert.Equal(t, fmt.Sprintf("%s%d", "dc", (i+1)), dcs[i].Name, "DeploymentConfig name does not have the expected value") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pkg/resource/write/hooks/update_hooks.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | corev1 "k8s.io/api/core/v1" 5 | "reflect" 6 | "sigs.k8s.io/controller-runtime/pkg/client" 7 | ) 8 | 9 | type hookFunc = func(existing client.Object, requested client.Object) error 10 | 11 | type UpdateHookMap struct { 12 | DefaultHook hookFunc 13 | HookMap map[reflect.Type]hookFunc 14 | } 15 | 16 | func DefaultUpdateHooks() *UpdateHookMap { 17 | hookMap := make(map[reflect.Type]func(existing client.Object, requested client.Object) error) 18 | hookMap[reflect.TypeOf(corev1.Service{})] = serviceHook 19 | return &UpdateHookMap{ 20 | DefaultHook: defaultHook, 21 | HookMap: hookMap, 22 | } 23 | } 24 | 25 | func (this *UpdateHookMap) Trigger(existing client.Object, requested client.Object) error { 26 | function := this.HookMap[reflect.ValueOf(existing).Elem().Type()] 27 | if function == nil { 28 | function = this.DefaultHook 29 | } 30 | return function(existing, requested) 31 | } 32 | 33 | func defaultHook(existing client.Object, requested client.Object) error { 34 | requested.SetResourceVersion(existing.GetResourceVersion()) 35 | requested.GetObjectKind().SetGroupVersionKind(existing.GetObjectKind().GroupVersionKind()) 36 | return nil 37 | } 38 | 39 | func serviceHook(existing client.Object, requested client.Object) error { 40 | existingService := existing.(*corev1.Service) 41 | requestedService := requested.(*corev1.Service) 42 | if requestedService.Spec.ClusterIP == "" { 43 | requestedService.Spec.ClusterIP = existingService.Spec.ClusterIP 44 | } 45 | err := defaultHook(existing, requested) 46 | if err != nil { 47 | return err 48 | } 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /pkg/resource/write/writer.go: -------------------------------------------------------------------------------- 1 | package write 2 | 3 | import ( 4 | "context" 5 | "github.com/RHsyseng/operator-utils/pkg/resource/write/hooks" 6 | newerror "github.com/pkg/errors" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 11 | ) 12 | 13 | type UpdateHooks interface { 14 | Trigger(existing client.Object, requested client.Object) error 15 | } 16 | 17 | type resourceWriter struct { 18 | writer client.Writer 19 | ownerRefs []metav1.OwnerReference 20 | ownerController metav1.Object 21 | scheme *runtime.Scheme 22 | updateHooks UpdateHooks 23 | } 24 | 25 | // New creates a resourceWriter object that can be used to add/update/remove kubernetes resources 26 | // the provided writer object will be used for the underlying operations 27 | func New(writer client.Writer) *resourceWriter { 28 | return &resourceWriter{ 29 | writer: writer, 30 | updateHooks: hooks.DefaultUpdateHooks(), 31 | } 32 | } 33 | 34 | // WithOwnerReferences allows owner references to be set on any object that's added or updated 35 | // calling this function removes any owner controller that may have been configured 36 | func (this *resourceWriter) WithOwnerReferences(ownerRefs ...metav1.OwnerReference) *resourceWriter { 37 | this.ownerRefs = ownerRefs 38 | this.ownerController = nil 39 | this.scheme = nil 40 | return this 41 | } 42 | 43 | // WithOwnerController allows a controlling owner to be set on any object that's added or updated 44 | // calling this function removes the effect of previously setting owner references on this resource writer 45 | func (this *resourceWriter) WithOwnerController(ownerController metav1.Object, scheme *runtime.Scheme) *resourceWriter { 46 | this.ownerController = ownerController 47 | this.scheme = scheme 48 | this.ownerRefs = nil 49 | return this 50 | } 51 | 52 | // WithCustomUpdateHooks allows intercepting update calls to set missing fields 53 | // default provided update hooks, for example, set the resource version and GVK based on existing counterpart 54 | func (this *resourceWriter) WithCustomUpdateHooks(updateHooks UpdateHooks) *resourceWriter { 55 | this.updateHooks = updateHooks 56 | return this 57 | } 58 | 59 | // AddResources sets ownership as/if configured, and then uses the writer to create them 60 | // the boolean result is true if any changes were made 61 | func (this *resourceWriter) AddResources(resources []client.Object) (bool, error) { 62 | var added bool 63 | for index := range resources { 64 | requested := resources[index] 65 | if this.ownerRefs != nil { 66 | requested.SetOwnerReferences(this.ownerRefs) 67 | } else if this.canSetOwnerRef(requested, this.ownerController) { 68 | err := controllerutil.SetControllerReference(this.ownerController, requested, this.scheme) 69 | if err != nil { 70 | return added, err 71 | } 72 | } 73 | err := this.writer.Create(context.TODO(), requested) 74 | if err != nil { 75 | return added, err 76 | } 77 | added = true 78 | } 79 | return added, nil 80 | } 81 | 82 | func (this *resourceWriter) canSetOwnerRef(resource metav1.Object, owner metav1.Object) bool { 83 | if owner == nil { 84 | return false 85 | } 86 | if resource.GetNamespace() == "" { 87 | return owner.GetNamespace() == "" 88 | } 89 | return owner.GetNamespace() != "" 90 | } 91 | 92 | // UpdateResources finds the updated counterpart for each of the provided resources in the existing array and uses it to set resource version and GVK 93 | // It also sets ownership as/if configured, and then uses the writer to update them 94 | // the boolean result is true if any changes were made 95 | func (this *resourceWriter) UpdateResources(existing []client.Object, resources []client.Object) (bool, error) { 96 | var updated bool 97 | for index := range resources { 98 | requested := resources[index] 99 | var counterpart client.Object 100 | for _, candidate := range existing { 101 | if candidate.GetNamespace() == requested.GetNamespace() && candidate.GetName() == requested.GetName() { 102 | counterpart = candidate 103 | break 104 | } 105 | } 106 | if counterpart == nil { 107 | return updated, newerror.New("Failed to find a deployed counterpart to resource being updated") 108 | } 109 | err := this.updateHooks.Trigger(counterpart, requested) 110 | if err != nil { 111 | return updated, err 112 | } 113 | if this.ownerRefs != nil { 114 | requested.SetOwnerReferences(this.ownerRefs) 115 | } else if this.ownerController != nil { 116 | err := controllerutil.SetControllerReference(this.ownerController, requested, this.scheme) 117 | if err != nil { 118 | return updated, err 119 | } 120 | } 121 | err = this.writer.Update(context.TODO(), requested) 122 | if err != nil { 123 | return updated, err 124 | } 125 | updated = true 126 | } 127 | return updated, nil 128 | } 129 | 130 | // RemoveResources removes each of the provided resources using the provided writer 131 | // the boolean result is true if any changes were made 132 | func (this *resourceWriter) RemoveResources(resources []client.Object) (bool, error) { 133 | var removed bool 134 | for index := range resources { 135 | err := this.writer.Delete(context.TODO(), resources[index]) 136 | if err != nil { 137 | return removed, err 138 | } 139 | removed = true 140 | } 141 | return removed, nil 142 | } 143 | -------------------------------------------------------------------------------- /pkg/resource/write/writer_test.go: -------------------------------------------------------------------------------- 1 | package write 2 | 3 | import ( 4 | "context" 5 | "github.com/stretchr/testify/assert" 6 | corev1 "k8s.io/api/core/v1" 7 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "k8s.io/apimachinery/pkg/types" 10 | "sigs.k8s.io/controller-runtime/pkg/client" 11 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 12 | "testing" 13 | ) 14 | 15 | func TestFluentAPI(t *testing.T) { 16 | scheme := getScheme(t) 17 | 18 | noOwnership := New(fake.NewClientBuilder().WithScheme(scheme).Build()) 19 | assert.Nil(t, noOwnership.ownerRefs, "Do not expect ownerRefs to be set") 20 | assert.Nil(t, noOwnership.ownerController, "Do not expect ownerController to be set") 21 | 22 | ownerRefs := New(fake.NewClientBuilder().WithScheme(scheme).Build()).WithOwnerController(&corev1.Service{}, scheme) 23 | assert.Nil(t, ownerRefs.ownerRefs, "Do not expect ownerRefs to be set") 24 | assert.NotNil(t, ownerRefs.ownerController, "Expect ownerController to be set") 25 | 26 | controler := New(fake.NewClientBuilder().WithScheme(scheme).Build()).WithOwnerReferences(v1.OwnerReference{}) 27 | assert.NotNil(t, controler.ownerRefs, "Expect ownerRefs to be set") 28 | assert.Nil(t, controler.ownerController, "Do not expect ownerController to be set") 29 | } 30 | 31 | func TestCreateService(t *testing.T) { 32 | scheme := getScheme(t) 33 | cli := fake.NewClientBuilder().WithScheme(scheme).Build() 34 | requestedService := corev1.Service{ 35 | ObjectMeta: v1.ObjectMeta{ 36 | Name: "service1", 37 | Namespace: "namespace", 38 | }, 39 | Spec: corev1.ServiceSpec{ 40 | SessionAffinity: corev1.ServiceAffinityClientIP, 41 | }, 42 | } 43 | requestedService.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Service")) 44 | added, err := New(cli).AddResources([]client.Object{&requestedService}) 45 | assert.Nil(t, err, "Expect no errors creating a simple object") 46 | assert.True(t, added, "Object should be added") 47 | 48 | existingService := corev1.Service{} 49 | err = cli.Get(context.TODO(), types.NamespacedName{Name: "service1", Namespace: "namespace"}, &existingService) 50 | assert.Nil(t, err, "Expect no errors loading existing object") 51 | assert.Equal(t, requestedService, existingService) 52 | } 53 | 54 | func TestUpdateService(t *testing.T) { 55 | scheme := getScheme(t) 56 | clusterIP := "1.2.3.4" 57 | cli := fake.NewClientBuilder().WithScheme(scheme).Build() 58 | requestedService := corev1.Service{ 59 | ObjectMeta: v1.ObjectMeta{ 60 | Name: "service1", 61 | Namespace: "namespace", 62 | }, 63 | Spec: corev1.ServiceSpec{ 64 | ClusterIP: clusterIP, 65 | SessionAffinity: corev1.ServiceAffinityClientIP, 66 | }, 67 | } 68 | requestedService.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Service")) 69 | added, err := New(cli).AddResources([]client.Object{&requestedService}) 70 | assert.Nil(t, err, "Expect no errors creating a simple object") 71 | assert.True(t, added, "Object should be added") 72 | 73 | updatedService := corev1.Service{ 74 | ObjectMeta: v1.ObjectMeta{ 75 | Name: "service1", 76 | Namespace: "namespace", 77 | }, 78 | Spec: corev1.ServiceSpec{ 79 | SessionAffinity: corev1.ServiceAffinityNone, 80 | }, 81 | } 82 | updatedService.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Service")) 83 | updated, err := New(cli).UpdateResources([]client.Object{&requestedService}, []client.Object{&updatedService}) 84 | assert.Nil(t, err, "Expect no errors updating object") 85 | assert.True(t, updated, "Object should be updated") 86 | 87 | existingService := corev1.Service{} 88 | err = cli.Get(context.TODO(), types.NamespacedName{Name: "service1", Namespace: "namespace"}, &existingService) 89 | assert.Nil(t, err, "Expect no errors loading existing object") 90 | //Update call should set the existing ClusterIP on the object before writing it: 91 | updatedService.Spec.ClusterIP = clusterIP 92 | assert.Equal(t, updatedService, existingService, "Expected Cluster IP to be set on the updating object") 93 | } 94 | 95 | func getScheme(t *testing.T) *runtime.Scheme { 96 | scheme := runtime.NewScheme() 97 | err := corev1.SchemeBuilder.AddToScheme(scheme) 98 | assert.Nil(t, err, "Expect no errors building scheme") 99 | return scheme 100 | } 101 | -------------------------------------------------------------------------------- /pkg/test/mock_imagestreamtag.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | imagev1 "github.com/openshift/api/image/v1" 8 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | type MockImageStreamTag struct { 12 | Tags map[string]*imagev1.ImageStreamTag 13 | } 14 | 15 | func (mock *MockImageStreamTag) Create(ctx context.Context, tag *imagev1.ImageStreamTag, options meta_v1.CreateOptions) (*imagev1.ImageStreamTag, error) { 16 | if mock.Tags == nil { 17 | mock.Tags = make(map[string]*imagev1.ImageStreamTag) 18 | } 19 | name := fmt.Sprintf("%s/%s", tag.ObjectMeta.Namespace, tag.ObjectMeta.Name) 20 | mock.Tags[name] = tag 21 | return tag, nil 22 | } 23 | 24 | func (mock *MockImageStreamTag) Update(ctx context.Context, tag *imagev1.ImageStreamTag, options meta_v1.UpdateOptions) (*imagev1.ImageStreamTag, error) { 25 | if mock.Tags == nil { 26 | mock.Tags = make(map[string]*imagev1.ImageStreamTag) 27 | } 28 | name := fmt.Sprintf("%s/%s", tag.ObjectMeta.Namespace, tag.ObjectMeta.Name) 29 | old := mock.Tags[name] 30 | mock.Tags[name] = tag 31 | return old, nil 32 | } 33 | 34 | func (mock *MockImageStreamTag) Delete(ctx context.Context, name string, options meta_v1.DeleteOptions) error { 35 | if mock.Tags == nil { 36 | return nil 37 | } 38 | delete(mock.Tags, name) 39 | return nil 40 | } 41 | 42 | func (mock *MockImageStreamTag) Get(ctx context.Context, name string, options meta_v1.GetOptions) (*imagev1.ImageStreamTag, error) { 43 | if mock.Tags == nil { 44 | return nil, nil 45 | } 46 | return mock.Tags[name], nil 47 | } 48 | 49 | func (mock *MockImageStreamTag) List(ctx context.Context, opts meta_v1.ListOptions) (*imagev1.ImageStreamTagList, error) { 50 | if mock.Tags == nil { 51 | return nil, nil 52 | } 53 | items := make([]imagev1.ImageStreamTag, 0, len(mock.Tags)) 54 | for _, val := range mock.Tags { 55 | items = append(items, *val) 56 | } 57 | list := &imagev1.ImageStreamTagList{ 58 | Items: items, 59 | } 60 | return list, nil 61 | } 62 | -------------------------------------------------------------------------------- /pkg/test/mock_service.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "context" 5 | 6 | oappsv1 "github.com/openshift/api/apps/v1" 7 | buildv1 "github.com/openshift/api/build/v1" 8 | consolev1 "github.com/openshift/api/console/v1" 9 | oimagev1 "github.com/openshift/api/image/v1" 10 | routev1 "github.com/openshift/api/route/v1" 11 | imagev1 "github.com/openshift/client-go/image/clientset/versioned/typed/image/v1" 12 | appsv1 "k8s.io/api/apps/v1" 13 | corev1 "k8s.io/api/core/v1" 14 | rbacv1 "k8s.io/api/rbac/v1" 15 | "k8s.io/apimachinery/pkg/runtime" 16 | "k8s.io/apimachinery/pkg/runtime/schema" 17 | ctrl "sigs.k8s.io/controller-runtime" 18 | clientv1 "sigs.k8s.io/controller-runtime/pkg/client" 19 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 20 | "sigs.k8s.io/controller-runtime/pkg/scheme" 21 | ) 22 | 23 | var log = ctrl.Log.WithName("operatorutils.test") 24 | 25 | type MockPlatformService struct { 26 | Client clientv1.Client 27 | scheme *runtime.Scheme 28 | CreateFunc func(ctx context.Context, obj clientv1.Object, opts ...clientv1.CreateOption) error 29 | DeleteFunc func(ctx context.Context, obj clientv1.Object, opts ...clientv1.DeleteOption) error 30 | GetFunc func(ctx context.Context, key clientv1.ObjectKey, obj clientv1.Object) error 31 | ListFunc func(ctx context.Context, list clientv1.ObjectList, opts ...clientv1.ListOption) error 32 | UpdateFunc func(ctx context.Context, obj clientv1.Object, opts ...clientv1.UpdateOption) error 33 | PatchFunc func(ctx context.Context, obj clientv1.Object, patch clientv1.Patch, opts ...clientv1.PatchOption) error 34 | DeleteAllOfFunc func(ctx context.Context, obj clientv1.Object, opts ...clientv1.DeleteAllOfOption) error 35 | GetCachedFunc func(ctx context.Context, key clientv1.ObjectKey, obj clientv1.Object) error 36 | ImageStreamTagsFunc func(namespace string) imagev1.ImageStreamTagInterface 37 | GetSchemeFunc func() *runtime.Scheme 38 | StatusFunc func() clientv1.StatusWriter 39 | } 40 | 41 | var knownTypes = map[schema.GroupVersion][]runtime.Object{ 42 | corev1.SchemeGroupVersion: { 43 | &corev1.PersistentVolumeClaim{}, 44 | &corev1.ServiceAccount{}, 45 | &corev1.Secret{}, 46 | &corev1.Service{}, 47 | &corev1.PersistentVolumeClaimList{}, 48 | &corev1.ServiceAccountList{}, 49 | &corev1.ServiceList{}}, 50 | oappsv1.GroupVersion: { 51 | &oappsv1.DeploymentConfig{}, 52 | &oappsv1.DeploymentConfigList{}, 53 | }, 54 | appsv1.SchemeGroupVersion: { 55 | &appsv1.StatefulSet{}, 56 | &appsv1.StatefulSetList{}, 57 | }, 58 | routev1.GroupVersion: { 59 | &routev1.Route{}, 60 | &routev1.RouteList{}, 61 | }, 62 | oimagev1.GroupVersion: { 63 | &oimagev1.ImageStream{}, 64 | &oimagev1.ImageStreamList{}, 65 | }, 66 | rbacv1.SchemeGroupVersion: { 67 | &rbacv1.Role{}, 68 | &rbacv1.RoleList{}, 69 | &rbacv1.RoleBinding{}, 70 | &rbacv1.RoleBindingList{}, 71 | }, 72 | buildv1.GroupVersion: { 73 | &buildv1.BuildConfig{}, 74 | &buildv1.BuildConfigList{}, 75 | }, 76 | consolev1.GroupVersion: { 77 | &consolev1.ConsoleLink{}, 78 | &consolev1.ConsoleLinkList{}, 79 | }, 80 | } 81 | 82 | type MockPlatformServiceBuilder struct { 83 | apiObjects []runtime.Object 84 | extraObjects []runtime.Object 85 | schemeBuilder *scheme.Builder 86 | } 87 | 88 | func NewMockPlatformServiceBuilder(schemeBuilder runtime.SchemeBuilder) *MockPlatformServiceBuilder { 89 | builder := &scheme.Builder{ 90 | GroupVersion: corev1.SchemeGroupVersion, 91 | SchemeBuilder: schemeBuilder, 92 | } 93 | return &MockPlatformServiceBuilder{schemeBuilder: builder} 94 | } 95 | 96 | func (builder *MockPlatformServiceBuilder) WithScheme(objs ...runtime.Object) { 97 | builder.apiObjects = objs 98 | } 99 | 100 | func (builder *MockPlatformServiceBuilder) WithExtraScheme(objs ...runtime.Object) { 101 | builder.extraObjects = objs 102 | } 103 | 104 | func (builder *MockPlatformServiceBuilder) Build() *MockPlatformService { 105 | registerObjs := builder.apiObjects 106 | registerObjs = append(registerObjs, builder.extraObjects...) 107 | builder.schemeBuilder.Register(registerObjs...) 108 | scheme, _ := builder.schemeBuilder.Build() 109 | for gv, types := range knownTypes { 110 | for _, t := range types { 111 | scheme.AddKnownTypes(gv, t) 112 | } 113 | } 114 | client := fake.NewFakeClientWithScheme(scheme) 115 | log.V(1).Info("Fake client created as %v", client) 116 | mockImageStreamTag := &MockImageStreamTag{} 117 | return &MockPlatformService{ 118 | Client: client, 119 | scheme: scheme, 120 | CreateFunc: func(ctx context.Context, obj clientv1.Object, opts ...clientv1.CreateOption) error { 121 | return client.Create(ctx, obj, opts...) 122 | }, 123 | DeleteFunc: func(ctx context.Context, obj clientv1.Object, opts ...clientv1.DeleteOption) error { 124 | return client.Delete(ctx, obj, opts...) 125 | }, 126 | GetFunc: func(ctx context.Context, key clientv1.ObjectKey, obj clientv1.Object) error { 127 | return client.Get(ctx, key, obj) 128 | }, 129 | ListFunc: func(ctx context.Context, list clientv1.ObjectList, opts ...clientv1.ListOption) error { 130 | return client.List(ctx, list, opts...) 131 | }, 132 | UpdateFunc: func(ctx context.Context, obj clientv1.Object, opts ...clientv1.UpdateOption) error { 133 | return client.Update(ctx, obj, opts...) 134 | }, 135 | PatchFunc: func(ctx context.Context, obj clientv1.Object, patch clientv1.Patch, opts ...clientv1.PatchOption) error { 136 | return client.Patch(ctx, obj, patch, opts...) 137 | }, 138 | DeleteAllOfFunc: func(ctx context.Context, obj clientv1.Object, opts ...clientv1.DeleteAllOfOption) error { 139 | return client.DeleteAllOf(ctx, obj, opts...) 140 | }, 141 | GetCachedFunc: func(ctx context.Context, key clientv1.ObjectKey, obj clientv1.Object) error { 142 | return client.Get(ctx, key, obj) 143 | }, 144 | ImageStreamTagsFunc: func(namespace string) imagev1.ImageStreamTagInterface { 145 | return mockImageStreamTag 146 | }, 147 | GetSchemeFunc: func() *runtime.Scheme { 148 | return scheme 149 | }, 150 | StatusFunc: func() clientv1.StatusWriter { 151 | return client.Status() 152 | }, 153 | } 154 | } 155 | 156 | func (service *MockPlatformService) Create(ctx context.Context, obj clientv1.Object, opts ...clientv1.CreateOption) error { 157 | return service.CreateFunc(ctx, obj, opts...) 158 | } 159 | 160 | func (service *MockPlatformService) Delete(ctx context.Context, obj clientv1.Object, opts ...clientv1.DeleteOption) error { 161 | return service.DeleteFunc(ctx, obj, opts...) 162 | } 163 | 164 | func (service *MockPlatformService) Get(ctx context.Context, key clientv1.ObjectKey, obj clientv1.Object) error { 165 | return service.GetFunc(ctx, key, obj) 166 | } 167 | 168 | func (service *MockPlatformService) List(ctx context.Context, list clientv1.ObjectList, opts ...clientv1.ListOption) error { 169 | return service.ListFunc(ctx, list, opts...) 170 | } 171 | 172 | func (service *MockPlatformService) Update(ctx context.Context, obj clientv1.Object, opts ...clientv1.UpdateOption) error { 173 | return service.UpdateFunc(ctx, obj, opts...) 174 | } 175 | 176 | func (service *MockPlatformService) Patch(ctx context.Context, obj clientv1.Object, patch clientv1.Patch, opts ...clientv1.PatchOption) error { 177 | return service.PatchFunc(ctx, obj, patch, opts...) 178 | } 179 | 180 | func (service *MockPlatformService) DeleteAllOf(ctx context.Context, obj clientv1.Object, opts ...clientv1.DeleteAllOfOption) error { 181 | return service.DeleteAllOfFunc(ctx, obj, opts...) 182 | } 183 | 184 | func (service *MockPlatformService) GetCached(ctx context.Context, key clientv1.ObjectKey, obj clientv1.Object) error { 185 | return service.GetCachedFunc(ctx, key, obj) 186 | } 187 | 188 | func (service *MockPlatformService) ImageStreamTags(namespace string) imagev1.ImageStreamTagInterface { 189 | return service.ImageStreamTagsFunc(namespace) 190 | } 191 | 192 | func (service *MockPlatformService) GetScheme() *runtime.Scheme { 193 | return service.GetSchemeFunc() 194 | } 195 | 196 | func (service *MockPlatformService) Status() clientv1.StatusWriter { 197 | return service.StatusFunc() 198 | } 199 | 200 | func (service *MockPlatformService) IsMockService() bool { 201 | return true 202 | } 203 | -------------------------------------------------------------------------------- /pkg/utils/kubernetes/api.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "context" 5 | 6 | imagev1 "github.com/openshift/client-go/image/clientset/versioned/typed/image/v1" 7 | "k8s.io/apimachinery/pkg/runtime" 8 | "sigs.k8s.io/controller-runtime/pkg/client" 9 | ) 10 | 11 | // PlatformService ... 12 | type PlatformService interface { 13 | Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error 14 | Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error 15 | Get(ctx context.Context, key client.ObjectKey, obj client.Object) error 16 | List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error 17 | Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error 18 | Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error 19 | DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error 20 | GetCached(ctx context.Context, key client.ObjectKey, obj client.Object) error 21 | ImageStreamTags(namespace string) imagev1.ImageStreamTagInterface 22 | GetScheme() *runtime.Scheme 23 | Status() client.StatusWriter 24 | IsMockService() bool 25 | } 26 | -------------------------------------------------------------------------------- /pkg/utils/kubernetes/finalizer.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "sigs.k8s.io/controller-runtime/pkg/client" 8 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 9 | "strings" 10 | ) 11 | 12 | type Finalizer interface { 13 | GetName() string 14 | OnFinalize(owner client.Object, service PlatformService) error 15 | } 16 | 17 | func (e *ExtendedReconciler) RegisterFinalizer(f Finalizer) error { 18 | err := validateFinalizerName(f.GetName()) 19 | if err != nil { 20 | return err 21 | } 22 | e.Finalizers[f.GetName()] = f 23 | return nil 24 | } 25 | 26 | func (e *ExtendedReconciler) UnregisterFinalizer(finalizer string) error { 27 | err := validateFinalizerName(finalizer) 28 | if err != nil { 29 | return err 30 | } 31 | delete(e.Finalizers, finalizer) 32 | return nil 33 | } 34 | 35 | // IsFinalizing An object is considered to be finalizing when its deletionTimestamp is not null 36 | func (e *ExtendedReconciler) isFinalizing(owner metav1.Object) bool { 37 | return owner.GetDeletionTimestamp() != nil 38 | } 39 | 40 | // RemoveFinalizer removes a finalizer and updates the owner object 41 | func (e *ExtendedReconciler) removeFinalizer(owner client.Object, finalizer string) error { 42 | err := validateFinalizerName(finalizer) 43 | if err != nil { 44 | return err 45 | } 46 | controllerutil.RemoveFinalizer(owner, finalizer) 47 | return e.Service.Update(context.TODO(), owner) 48 | } 49 | 50 | // FinalizeOnDelete triggers all the finalizers registered for the given object in case it is being deleted 51 | func (e *ExtendedReconciler) finalizeOnDelete(owner client.Object) error { 52 | if !e.isFinalizing(owner) { 53 | return nil 54 | } 55 | for _, f := range owner.GetFinalizers() { 56 | finalizer := e.Finalizers[f] 57 | if finalizer != nil { 58 | err := finalizer.OnFinalize(owner, e.Service) 59 | if err != nil { 60 | return err 61 | } 62 | err = e.removeFinalizer(owner, f) 63 | if err != nil { 64 | return err 65 | } 66 | } else { 67 | return fmt.Errorf("finalizer %s does not have a Finalizer handler registered", finalizer) 68 | } 69 | } 70 | return nil 71 | } 72 | 73 | func validateFinalizerName(name string) error { 74 | if len(strings.TrimSpace(name)) == 0 { 75 | return fmt.Errorf("the finalizer name must not be empty") 76 | } 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /pkg/utils/kubernetes/kube_service.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "context" 5 | 6 | imagev1 "github.com/openshift/client-go/image/clientset/versioned/typed/image/v1" 7 | "k8s.io/apimachinery/pkg/runtime" 8 | ctrl "sigs.k8s.io/controller-runtime" 9 | cachev1 "sigs.k8s.io/controller-runtime/pkg/cache" 10 | clientv1 "sigs.k8s.io/controller-runtime/pkg/client" 11 | "sigs.k8s.io/controller-runtime/pkg/manager" 12 | ) 13 | 14 | var log = ctrl.Log.WithName("operatorutils.kubernetes") 15 | 16 | type KubernetesPlatformService struct { 17 | client clientv1.Client 18 | cache cachev1.Cache 19 | imageClient *imagev1.ImageV1Client 20 | scheme *runtime.Scheme 21 | } 22 | 23 | func GetInstance(mgr manager.Manager) KubernetesPlatformService { 24 | imageClient, err := imagev1.NewForConfig(mgr.GetConfig()) 25 | if err != nil { 26 | log.Error(err, "Error getting image client") 27 | return KubernetesPlatformService{} 28 | } 29 | 30 | return KubernetesPlatformService{ 31 | client: mgr.GetClient(), 32 | cache: mgr.GetCache(), 33 | imageClient: imageClient, 34 | scheme: mgr.GetScheme(), 35 | } 36 | } 37 | 38 | func (service *KubernetesPlatformService) Create(ctx context.Context, obj clientv1.Object, opts ...clientv1.CreateOption) error { 39 | return service.client.Create(ctx, obj, opts...) 40 | } 41 | 42 | func (service *KubernetesPlatformService) Delete(ctx context.Context, obj clientv1.Object, opts ...clientv1.DeleteOption) error { 43 | return service.client.Delete(ctx, obj, opts...) 44 | } 45 | 46 | func (service *KubernetesPlatformService) Get(ctx context.Context, key clientv1.ObjectKey, obj clientv1.Object) error { 47 | return service.client.Get(ctx, key, obj) 48 | } 49 | 50 | func (service *KubernetesPlatformService) List(ctx context.Context, list clientv1.ObjectList, opts ...clientv1.ListOption) error { 51 | return service.client.List(ctx, list, opts...) 52 | } 53 | 54 | func (service *KubernetesPlatformService) Update(ctx context.Context, obj clientv1.Object, opts ...clientv1.UpdateOption) error { 55 | return service.client.Update(ctx, obj, opts...) 56 | } 57 | 58 | func (service *KubernetesPlatformService) Patch(ctx context.Context, obj clientv1.Object, patch clientv1.Patch, opts ...clientv1.PatchOption) error { 59 | return service.client.Patch(ctx, obj, patch, opts...) 60 | } 61 | 62 | func (service *KubernetesPlatformService) DeleteAllOf(ctx context.Context, obj clientv1.Object, opts ...clientv1.DeleteAllOfOption) error { 63 | return service.client.DeleteAllOf(ctx, obj, opts...) 64 | } 65 | 66 | func (service *KubernetesPlatformService) GetCached(ctx context.Context, key clientv1.ObjectKey, obj clientv1.Object) error { 67 | return service.cache.Get(ctx, key, obj) 68 | } 69 | 70 | func (service *KubernetesPlatformService) ImageStreamTags(namespace string) imagev1.ImageStreamTagInterface { 71 | return service.imageClient.ImageStreamTags(namespace) 72 | } 73 | 74 | func (service *KubernetesPlatformService) GetScheme() *runtime.Scheme { 75 | return service.scheme 76 | } 77 | 78 | func (service *KubernetesPlatformService) Status() clientv1.StatusWriter { 79 | return service.client.Status() 80 | } 81 | 82 | func (service *KubernetesPlatformService) IsMockService() bool { 83 | return false 84 | } 85 | -------------------------------------------------------------------------------- /pkg/utils/kubernetes/reconciler.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "context" 5 | "k8s.io/apimachinery/pkg/api/errors" 6 | "sigs.k8s.io/controller-runtime/pkg/client" 7 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 8 | ) 9 | 10 | type ExtendedReconciler struct { 11 | Service PlatformService 12 | Reconciler reconcile.Reconciler 13 | Resource client.Object 14 | Finalizers map[string]Finalizer 15 | } 16 | 17 | func NewExtendedReconciler(service PlatformService, reconciler reconcile.Reconciler, resource client.Object) ExtendedReconciler { 18 | return ExtendedReconciler{ 19 | Service: service, 20 | Reconciler: reconciler, 21 | Resource: resource, 22 | Finalizers: map[string]Finalizer{}, 23 | } 24 | } 25 | 26 | func (e *ExtendedReconciler) Reconcile(request reconcile.Request) (reconcile.Result, error) { 27 | instance := e.Resource.DeepCopyObject().(client.Object) 28 | err := e.Service.Get(context.TODO(), request.NamespacedName, instance) 29 | if err != nil { 30 | if errors.IsNotFound(err) { 31 | // Request object not found, could have been deleted after reconcile request. 32 | // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. 33 | // Return and don't requeue 34 | return reconcile.Result{}, nil 35 | } 36 | // Error reading the object - requeue the request. 37 | return reconcile.Result{}, err 38 | } 39 | err = e.finalizeOnDelete(instance) 40 | if err != nil { 41 | return reconcile.Result{}, err 42 | } 43 | return e.Reconciler.Reconcile(context.TODO(), request) 44 | } 45 | -------------------------------------------------------------------------------- /pkg/utils/kubernetes/reconciler_test.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/RHsyseng/operator-utils/pkg/test" 7 | "github.com/stretchr/testify/assert" 8 | v1 "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/types" 11 | "sigs.k8s.io/controller-runtime/pkg/client" 12 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 13 | "testing" 14 | ) 15 | 16 | type MockReconciler struct { 17 | ReconcileFn func(context context.Context, request reconcile.Request) (reconcile.Result, error) 18 | } 19 | 20 | func (r *MockReconciler) Reconcile(context context.Context, request reconcile.Request) (reconcile.Result, error) { 21 | return reconcile.Result{}, nil 22 | } 23 | 24 | type MockFinalizer struct { 25 | name string 26 | onFinalizeFn func(owner client.Object, service PlatformService) error 27 | } 28 | 29 | func (m *MockFinalizer) GetName() string { 30 | return m.name 31 | } 32 | 33 | func (m *MockFinalizer) OnFinalize(owner client.Object, service PlatformService) error { 34 | if m.onFinalizeFn == nil { 35 | return nil 36 | } 37 | return m.onFinalizeFn(owner, service) 38 | } 39 | 40 | func (m *MockFinalizer) setOnFinalizeFn(onFinalizeFn func(owner client.Object, service PlatformService) error) { 41 | m.onFinalizeFn = onFinalizeFn 42 | } 43 | 44 | func TestExtendedReconciler_IsFinalizing(t *testing.T) { 45 | extReconciler := BuildTestExtendedReconciler() 46 | 47 | assert.False(t, extReconciler.isFinalizing(extReconciler.Resource)) 48 | 49 | extReconciler.Resource.SetDeletionTimestamp(&metav1.Time{}) 50 | assert.True(t, extReconciler.isFinalizing(extReconciler.Resource)) 51 | } 52 | 53 | func TestExtendedReconciler_RegisterFinalizer(t *testing.T) { 54 | extReconciler := BuildTestExtendedReconciler() 55 | 56 | assert.Len(t, extReconciler.Finalizers, 0) 57 | 58 | err := extReconciler.RegisterFinalizer(&MockFinalizer{}) 59 | assert.Errorf(t, err, "the finalizer name must not be empty") 60 | assert.Len(t, extReconciler.Finalizers, 0) 61 | 62 | err = extReconciler.RegisterFinalizer(&MockFinalizer{ 63 | name: "finalizer1", 64 | }) 65 | assert.Nil(t, err) 66 | assert.Len(t, extReconciler.Finalizers, 1) 67 | 68 | err = extReconciler.RegisterFinalizer(&MockFinalizer{ 69 | name: "finalizer2", 70 | }) 71 | assert.Nil(t, err) 72 | assert.Len(t, extReconciler.Finalizers, 2) 73 | 74 | err = extReconciler.RegisterFinalizer(&MockFinalizer{ 75 | name: "finalizer2", 76 | }) 77 | assert.Nil(t, err) 78 | assert.Len(t, extReconciler.Finalizers, 2) 79 | } 80 | 81 | func TestExtendedReconciler_UnregisterFinalizer(t *testing.T) { 82 | extReconciler := BuildTestExtendedReconciler() 83 | extReconciler.Finalizers = map[string]Finalizer{ 84 | "f1": &MockFinalizer{}, 85 | "f2": &MockFinalizer{}, 86 | } 87 | 88 | err := extReconciler.UnregisterFinalizer("") 89 | assert.Errorf(t, err, "the finalizer name must not be empty") 90 | assert.Len(t, extReconciler.Finalizers, 2) 91 | 92 | err = extReconciler.UnregisterFinalizer("f1") 93 | assert.Nil(t, err) 94 | assert.Len(t, extReconciler.Finalizers, 1) 95 | 96 | err = extReconciler.UnregisterFinalizer("f1") 97 | assert.Nil(t, err) 98 | assert.Len(t, extReconciler.Finalizers, 1) 99 | 100 | err = extReconciler.UnregisterFinalizer("f2") 101 | assert.Nil(t, err) 102 | assert.Len(t, extReconciler.Finalizers, 0) 103 | } 104 | 105 | func TestExtendedReconciler_FinalizeOnDelete(t *testing.T) { 106 | extReconciler := BuildTestExtendedReconciler() 107 | extReconciler.Finalizers = map[string]Finalizer{ 108 | "f1": &MockFinalizer{}, 109 | "f2": &MockFinalizer{}, 110 | } 111 | 112 | pod := &v1.Pod{ 113 | ObjectMeta: metav1.ObjectMeta{ 114 | Name: "somepod", 115 | Namespace: "somenamespace", 116 | }, 117 | } 118 | pod.SetFinalizers([]string{"f1", "f2"}) 119 | 120 | extReconciler.Service.Create(context.TODO(), pod) 121 | 122 | err := extReconciler.finalizeOnDelete(pod) 123 | assert.Nil(t, err) 124 | assert.Len(t, pod.GetFinalizers(), 2) 125 | assert.Len(t, extReconciler.Finalizers, 2) 126 | 127 | pod.SetDeletionTimestamp(&metav1.Time{}) 128 | extReconciler.Service.Update(context.TODO(), pod) 129 | 130 | err = extReconciler.finalizeOnDelete(pod) 131 | assert.Nil(t, err) 132 | assert.Empty(t, pod.GetFinalizers()) 133 | assert.Len(t, extReconciler.Finalizers, 2) 134 | } 135 | 136 | func TestExtendedReconciler_FinalizeOnDeleteUnregisteredFinalizer(t *testing.T) { 137 | extReconciler := BuildTestExtendedReconciler() 138 | extReconciler.Finalizers = map[string]Finalizer{ 139 | "f1": &MockFinalizer{}, 140 | } 141 | 142 | pod := &v1.Pod{ 143 | ObjectMeta: metav1.ObjectMeta{ 144 | Name: "somepod", 145 | Namespace: "somenamespace", 146 | }, 147 | } 148 | pod.SetFinalizers([]string{"f1", "f2"}) 149 | pod.SetDeletionTimestamp(&metav1.Time{}) 150 | extReconciler.Service.Create(context.TODO(), pod) 151 | 152 | err := extReconciler.finalizeOnDelete(pod) 153 | assert.Errorf(t, err, "finalizer f2 does not have a Finalizer handler registered") 154 | 155 | newPod := &v1.Pod{} 156 | err = extReconciler.Service.Get(context.TODO(), types.NamespacedName{Name: pod.GetName(), Namespace: pod.GetNamespace()}, newPod) 157 | assert.Nil(t, err) 158 | assert.Len(t, newPod.GetFinalizers(), 1) 159 | assert.Len(t, extReconciler.Finalizers, 1) 160 | } 161 | 162 | func TestExtendedReconciler_FinalizeOnDeleteErrorOnFinalize(t *testing.T) { 163 | extReconciler := BuildTestExtendedReconciler() 164 | extReconciler.Finalizers = map[string]Finalizer{ 165 | "f1": &MockFinalizer{ 166 | onFinalizeFn: func(owner client.Object, service PlatformService) error { 167 | return fmt.Errorf("Foo error") 168 | }, 169 | }, 170 | } 171 | 172 | pod := &v1.Pod{ 173 | ObjectMeta: metav1.ObjectMeta{ 174 | Name: "somepod", 175 | Namespace: "somenamespace", 176 | }, 177 | } 178 | pod.SetFinalizers([]string{"f1"}) 179 | pod.SetDeletionTimestamp(&metav1.Time{}) 180 | extReconciler.Service.Create(context.TODO(), pod) 181 | 182 | err := extReconciler.finalizeOnDelete(pod) 183 | assert.Errorf(t, err, "Foo error") 184 | 185 | newPod := &v1.Pod{} 186 | err = extReconciler.Service.Get(context.TODO(), types.NamespacedName{Name: pod.GetName(), Namespace: pod.GetNamespace()}, newPod) 187 | assert.Nil(t, err) 188 | assert.Len(t, newPod.GetFinalizers(), 1) 189 | assert.Len(t, extReconciler.Finalizers, 1) 190 | } 191 | 192 | func TestExtendedReconciler_FinalizeOnDeleteErrorOnUpdate(t *testing.T) { 193 | mockService := BuildMockPlatformService() 194 | mockService.UpdateFunc = func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { 195 | return fmt.Errorf("Foo error") 196 | } 197 | extReconciler := BuildTestExtendedReconciler() 198 | extReconciler.Service = mockService 199 | extReconciler.Finalizers = map[string]Finalizer{ 200 | "f1": &MockFinalizer{}, 201 | } 202 | 203 | pod := &v1.Pod{ 204 | ObjectMeta: metav1.ObjectMeta{ 205 | Name: "somepod", 206 | Namespace: "somenamespace", 207 | }, 208 | } 209 | pod.SetFinalizers([]string{"f1"}) 210 | pod.SetDeletionTimestamp(&metav1.Time{}) 211 | extReconciler.Service.Create(context.TODO(), pod) 212 | 213 | err := extReconciler.finalizeOnDelete(pod) 214 | assert.Errorf(t, err, "Foo error") 215 | 216 | newPod := &v1.Pod{} 217 | err = mockService.Get(context.TODO(), types.NamespacedName{Name: pod.GetName(), Namespace: pod.GetNamespace()}, newPod) 218 | assert.Nil(t, err) 219 | assert.Len(t, newPod.GetFinalizers(), 1) 220 | assert.Len(t, extReconciler.Finalizers, 1) 221 | } 222 | 223 | func TestExtendedReconciler_Reconcile(t *testing.T) { 224 | extReconciler := BuildTestExtendedReconciler() 225 | var f1Invoked, f2Invoked bool 226 | f1 := MockFinalizer{name: "f1", onFinalizeFn: func(owner client.Object, service PlatformService) error { 227 | f1Invoked = true 228 | return nil 229 | }} 230 | f2 := MockFinalizer{name: "f2", onFinalizeFn: func(owner client.Object, service PlatformService) error { 231 | f2Invoked = true 232 | return nil 233 | }} 234 | extReconciler.Finalizers = map[string]Finalizer{ 235 | "f1": &f1, 236 | "f2": &f2, 237 | } 238 | pod := &v1.Pod{ 239 | ObjectMeta: metav1.ObjectMeta{ 240 | Name: "somepod", 241 | Namespace: "somenamespace", 242 | }, 243 | } 244 | pod.SetFinalizers([]string{"f1", "f2"}) 245 | 246 | extReconciler.Service.Create(context.TODO(), pod) 247 | 248 | request := reconcile.Request{} 249 | request.Namespace = pod.GetNamespace() 250 | request.Name = pod.GetName() 251 | result, err := extReconciler.Reconcile(request) 252 | assert.Nil(t, err) 253 | assert.Equal(t, reconcile.Result{}, result) 254 | assert.Len(t, pod.GetFinalizers(), 2) 255 | assert.Len(t, extReconciler.Finalizers, 2) 256 | 257 | time := metav1.Now() 258 | pod.SetDeletionTimestamp(&time) 259 | extReconciler.Service.Update(context.TODO(), pod) 260 | 261 | result, err = extReconciler.Reconcile(request) 262 | assert.Nil(t, err) 263 | assert.Equal(t, reconcile.Result{}, result) 264 | 265 | newPod := &v1.Pod{} 266 | err = extReconciler.Service.Get(context.TODO(), request.NamespacedName, newPod) 267 | assert.NotNil(t, err) 268 | assert.Len(t, newPod.GetFinalizers(), 0) 269 | assert.Len(t, extReconciler.Finalizers, 2) 270 | assert.True(t, f1Invoked) 271 | assert.True(t, f2Invoked) 272 | } 273 | 274 | func BuildTestExtendedReconciler() ExtendedReconciler { 275 | service := BuildMockPlatformService() 276 | reconciler := &MockReconciler{} 277 | customResource := &v1.Pod{} 278 | return NewExtendedReconciler(service, reconciler, customResource) 279 | } 280 | 281 | func BuildMockPlatformService() *test.MockPlatformService { 282 | return test.NewMockPlatformServiceBuilder(v1.SchemeBuilder).Build() 283 | } 284 | -------------------------------------------------------------------------------- /pkg/utils/kubernetes/utils.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "errors" 5 | "k8s.io/apimachinery/pkg/runtime/schema" 6 | "k8s.io/client-go/discovery" 7 | "sigs.k8s.io/controller-runtime/pkg/client/config" 8 | ) 9 | 10 | func CustomResourceDefinitionExists(gvk schema.GroupVersionKind) error { 11 | cfg, err := config.GetConfig() 12 | if err != nil { 13 | return err 14 | } 15 | client, err := discovery.NewDiscoveryClientForConfig(cfg) 16 | if err != nil { 17 | return err 18 | } 19 | return customResourceDefinitionExists(gvk, client) 20 | } 21 | 22 | func customResourceDefinitionExists(gvk schema.GroupVersionKind, client discovery.ServerResourcesInterface) error { 23 | api, err := client.ServerResourcesForGroupVersion(gvk.GroupVersion().String()) 24 | if err != nil { 25 | return err 26 | } 27 | for _, a := range api.APIResources { 28 | if a.Kind == gvk.Kind { 29 | return nil 30 | } 31 | } 32 | return errors.New(gvk.String() + " Kind not found ") 33 | } 34 | -------------------------------------------------------------------------------- /pkg/utils/kubernetes/utils_test.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | "k8s.io/client-go/discovery/fake" 8 | k8sTesting "k8s.io/client-go/testing" 9 | "testing" 10 | ) 11 | 12 | func TestCustomResourceDefinitionExists(t *testing.T) { 13 | client := &fake.FakeDiscovery{ 14 | Fake: &k8sTesting.Fake{}, 15 | FakedServerVersion: nil, 16 | } 17 | client.Resources = []*metav1.APIResourceList{ 18 | { 19 | TypeMeta: metav1.TypeMeta{}, 20 | GroupVersion: "console.openshift.io/v1", 21 | APIResources: []metav1.APIResource{{Kind: "ConsoleYAMLSample"}}, 22 | }, 23 | } 24 | gvk := schema.GroupVersionKind{Group: "console.openshift.io", Version: "v1", Kind: "ConsoleYAMLSample"} 25 | err := customResourceDefinitionExists(gvk, client) 26 | assert.Nil(t, err, "Failed to find ", gvk) 27 | 28 | gvk = schema.GroupVersionKind{Group: "console.openshift.io", Version: "v2", Kind: "ConsoleYAMLSample"} 29 | err = customResourceDefinitionExists(gvk, client) 30 | assert.NotNil(t, err, "Did not expect to find ", gvk) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/utils/openshift/utils.go: -------------------------------------------------------------------------------- 1 | package openshift 2 | 3 | import ( 4 | "github.com/RHsyseng/operator-utils/internal/platform" 5 | "k8s.io/client-go/rest" 6 | ) 7 | 8 | /* 9 | GetPlatformInfo examines the Kubernetes-based environment and determines the running platform, version, & OS. 10 | Accepts or instantiated 'cfg' rest config parameter. 11 | 12 | Result: PlatformInfo{ Name: OpenShift, K8SVersion: 1.13+, OS: linux/amd64 } 13 | */ 14 | func GetPlatformInfo(cfg *rest.Config) (platform.PlatformInfo, error) { 15 | return platform.K8SBasedPlatformVersioner{}.GetPlatformInfo(nil, cfg) 16 | } 17 | 18 | /* 19 | IsOpenShift is a helper method to simplify boolean OCP checks against GetPlatformInfo results 20 | Accepts or instantiated 'cfg' rest config parameter. 21 | */ 22 | func IsOpenShift(cfg *rest.Config) (bool, error) { 23 | info, err := GetPlatformInfo(cfg) 24 | if err != nil { 25 | return false, err 26 | } 27 | return info.IsOpenShift(), nil 28 | } 29 | 30 | /* 31 | GetPlatformName is a helper method to return the platform name from GetPlatformInfo results 32 | Accepts or instantiated 'cfg' rest config parameter. 33 | */ 34 | func GetPlatformName(cfg *rest.Config) (string, error) { 35 | info, err := GetPlatformInfo(cfg) 36 | if err != nil { 37 | return "", err 38 | } 39 | return string(info.Name), nil 40 | } 41 | 42 | /* 43 | LookupOpenShiftVersion fetches OpenShift version info from API endpoints 44 | *** NOTE: OCP 4.1+ requires elevated user permissions, see PlatformVersioner for details 45 | Accepts or instantiated 'cfg' rest config parameter. 46 | 47 | Result: OpenShiftVersion{ Version: 4.1.2 } 48 | */ 49 | func LookupOpenShiftVersion(cfg *rest.Config) (platform.OpenShiftVersion, error) { 50 | return platform.K8SBasedPlatformVersioner{}.LookupOpenShiftVersion(nil, cfg) 51 | } 52 | 53 | /* 54 | Supported platform: OpenShift 55 | cfg : OpenShift platform config, use runtime config if nil is passed in. 56 | version: Supported version format : Major.Minor 57 | 58 | e.g.: 4.3 59 | */ 60 | func CompareOpenShiftVersion(cfg *rest.Config, version string) (int, error) { 61 | return platform.K8SBasedPlatformVersioner{}.CompareOpenShiftVersion(nil, cfg, version) 62 | } 63 | 64 | /* 65 | MapKnownVersion maps from K8S version of PlatformInfo to equivalent OpenShift version 66 | 67 | Result: OpenShiftVersion{ Version: 4.1.2 } 68 | */ 69 | func MapKnownVersion(info platform.PlatformInfo) platform.OpenShiftVersion { 70 | return platform.MapKnownVersion(info) 71 | } 72 | -------------------------------------------------------------------------------- /pkg/utils/openshift/utils_test.go: -------------------------------------------------------------------------------- 1 | package openshift 2 | 3 | import ( 4 | "github.com/RHsyseng/operator-utils/internal/platform" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestOpenShiftVersion_MapKnownVersion(t *testing.T) { 10 | 11 | cases := []struct { 12 | label string 13 | info platform.PlatformInfo 14 | expectedOCPVersion string 15 | }{ 16 | { 17 | label: "case 1", 18 | info: platform.PlatformInfo{K8SVersion: ""}, 19 | expectedOCPVersion: "", 20 | }, 21 | { 22 | label: "case 2", 23 | info: platform.PlatformInfo{K8SVersion: "1.10+"}, 24 | expectedOCPVersion: "3.10", 25 | }, 26 | { 27 | label: "case 3", 28 | info: platform.PlatformInfo{K8SVersion: "1.11+"}, 29 | expectedOCPVersion: "3.11", 30 | }, 31 | { 32 | label: "case 4", 33 | info: platform.PlatformInfo{K8SVersion: "1.13+"}, 34 | expectedOCPVersion: "4.1", 35 | }, 36 | } 37 | 38 | for _, v := range cases { 39 | assert.Equal(t, v.expectedOCPVersion, MapKnownVersion(v.info).Version, v.label+": expected OCP version to match") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /pkg/utils/openshift/webconsole.go: -------------------------------------------------------------------------------- 1 | package openshift 2 | 3 | import ( 4 | "errors" 5 | "github.com/ghodss/yaml" 6 | consolev1 "github.com/openshift/api/console/v1" 7 | amv1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "sigs.k8s.io/controller-runtime/pkg/client" 9 | "strconv" 10 | ) 11 | 12 | func GetConsoleYAMLSample(res client.Object) (*consolev1.ConsoleYAMLSample, error) { 13 | annotations := res.GetAnnotations() 14 | snippetStr := annotations["consoleSnippet"] 15 | var snippet bool = false 16 | if tmp, err := strconv.ParseBool(snippetStr); err == nil { 17 | snippet = tmp 18 | } 19 | 20 | targetAPIVersion, _ := annotations["consoleTargetAPIVersion"] 21 | if targetAPIVersion == "" { 22 | targetAPIVersion = res.GetObjectKind().GroupVersionKind().GroupVersion().String() 23 | } 24 | 25 | targetKind := annotations["consoleTargetKind"] 26 | if targetKind == "" { 27 | targetKind = res.GetObjectKind().GroupVersionKind().Kind 28 | } 29 | 30 | defaultText := res.GetName() + "-yamlsample" 31 | title, _ := annotations["consoleTitle"] 32 | if title == "" { 33 | title = defaultText 34 | } 35 | desc, _ := annotations["consoleDesc"] 36 | if desc == "" { 37 | desc = defaultText 38 | } 39 | name, _ := annotations["consoleName"] 40 | if name == "" { 41 | name = defaultText 42 | } 43 | 44 | delete(annotations, "consoleSnippet") 45 | delete(annotations, "consoleTitle") 46 | delete(annotations, "consoleDesc") 47 | delete(annotations, "consoleName") 48 | delete(annotations, "consoleTargetAPIVersion") 49 | delete(annotations, "consoleTargetKind") 50 | 51 | data, err := yaml.Marshal(res) 52 | if err != nil { 53 | return nil, errors.New("Failed to convert to yamlstr from KubernetesResource.") 54 | } 55 | 56 | yamlSample := &consolev1.ConsoleYAMLSample{ 57 | ObjectMeta: amv1.ObjectMeta{ 58 | Name: name, 59 | Namespace: "openshift-console", 60 | }, 61 | Spec: consolev1.ConsoleYAMLSampleSpec{ 62 | TargetResource: amv1.TypeMeta{ 63 | APIVersion: targetAPIVersion, 64 | Kind: targetKind, 65 | }, 66 | Title: consolev1.ConsoleYAMLSampleTitle(title), 67 | Description: consolev1.ConsoleYAMLSampleDescription(desc), 68 | YAML: consolev1.ConsoleYAMLSampleYAML(string(data)), 69 | Snippet: snippet, 70 | }, 71 | } 72 | return yamlSample, nil 73 | } 74 | -------------------------------------------------------------------------------- /pkg/utils/openshift/webconsole_test.go: -------------------------------------------------------------------------------- 1 | package openshift 2 | 3 | import ( 4 | "github.com/ghodss/yaml" 5 | oappsv1 "github.com/openshift/api/apps/v1" 6 | v1 "github.com/openshift/api/console/v1" 7 | "github.com/stretchr/testify/assert" 8 | appsv1 "k8s.io/api/apps/v1" 9 | "testing" 10 | ) 11 | 12 | func TestGetConsoleYAMLSample(t *testing.T) { 13 | var inputYaml = ` 14 | apiVersion: v1 15 | kind: DeploymentConfig 16 | metadata: 17 | name: sample-dc 18 | annotations: 19 | consoleName: sample-deploymentconfig 20 | consoleDesc: Sample Deployment Config 21 | consoleTitle: Sample Deployment Config 22 | spec: 23 | replicas: 2 24 | ` 25 | original := &oappsv1.DeploymentConfig{} 26 | assert.NoError(t, yaml.Unmarshal([]byte(inputYaml), original)) 27 | 28 | yamlSample, err := GetConsoleYAMLSample(original) 29 | assert.NoError(t, err) 30 | 31 | assert.Equal(t, "sample-deploymentconfig", yamlSample.ObjectMeta.Name) 32 | assert.Equal(t, "openshift-console", yamlSample.ObjectMeta.Namespace) 33 | assert.Equal(t, "v1", yamlSample.Spec.TargetResource.APIVersion) 34 | assert.Equal(t, "DeploymentConfig", yamlSample.Spec.TargetResource.Kind) 35 | assert.Equal(t, v1.ConsoleYAMLSampleTitle("Sample Deployment Config"), yamlSample.Spec.Title) 36 | assert.Equal(t, v1.ConsoleYAMLSampleDescription("Sample Deployment Config"), yamlSample.Spec.Description) 37 | 38 | yamlContent := yamlSample.Spec.YAML 39 | actual := &oappsv1.DeploymentConfig{} 40 | assert.NoError(t, yaml.Unmarshal([]byte(string(yamlContent)), actual)) 41 | 42 | original.SetAnnotations(nil) 43 | assert.EqualValues(t, original, actual, "original yaml should be the same as the actual yaml") 44 | } 45 | 46 | func TestGetConsoleYAMLSampleWithNoAnnotations(t *testing.T) { 47 | var inputYaml = ` 48 | apiVersion: apps/v1 49 | kind: Deployment 50 | metadata: 51 | name: nginx-deployment 52 | labels: 53 | app: nginx 54 | spec: 55 | replicas: 3 56 | ` 57 | original := &appsv1.Deployment{} 58 | assert.NoError(t, yaml.Unmarshal([]byte(inputYaml), original)) 59 | 60 | yamlSample, err := GetConsoleYAMLSample(original) 61 | assert.NoError(t, err) 62 | 63 | assert.Equal(t, "nginx-deployment-yamlsample", yamlSample.ObjectMeta.Name) 64 | assert.Equal(t, "openshift-console", yamlSample.ObjectMeta.Namespace) 65 | assert.Equal(t, "apps/v1", yamlSample.Spec.TargetResource.APIVersion) 66 | assert.Equal(t, "Deployment", yamlSample.Spec.TargetResource.Kind) 67 | assert.Equal(t, v1.ConsoleYAMLSampleTitle("nginx-deployment-yamlsample"), yamlSample.Spec.Title) 68 | assert.Equal(t, v1.ConsoleYAMLSampleDescription("nginx-deployment-yamlsample"), yamlSample.Spec.Description) 69 | 70 | yamlContent := yamlSample.Spec.YAML 71 | actual := &appsv1.Deployment{} 72 | assert.NoError(t, yaml.Unmarshal([]byte(string(yamlContent)), actual)) 73 | 74 | assert.EqualValues(t, original, actual, "original yaml should be the same as the actual yaml") 75 | } 76 | -------------------------------------------------------------------------------- /pkg/validation/schema.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ghodss/yaml" 7 | "github.com/go-openapi/spec" 8 | "github.com/go-openapi/strfmt" 9 | "github.com/go-openapi/validate" 10 | ) 11 | 12 | type Schema interface { 13 | GetMissingEntries(crInstance interface{}) []SchemaEntry 14 | Validate(data interface{}) error 15 | } 16 | 17 | func New(crd []byte) (Schema, error) { 18 | object := &customResourceDefinition{} 19 | err := yaml.Unmarshal(crd, object) 20 | if err != nil { 21 | return nil, err 22 | } 23 | return &openAPIV3Schema{&object.Spec.Validation.OpenAPIV3Schema}, nil 24 | } 25 | 26 | func NewVersioned(crd []byte, version string) (Schema, error) { 27 | object := &customResourceDefinition{} 28 | err := yaml.Unmarshal(crd, object) 29 | if err != nil { 30 | return nil, err 31 | } 32 | for _, v := range object.Spec.Versions { 33 | if v.Name == version { 34 | return &openAPIV3Schema{&v.Schema.OpenAPIV3Schema}, nil 35 | } 36 | } 37 | return &openAPIV3Schema{}, fmt.Errorf("no version %s detected in crd", version) 38 | } 39 | 40 | type openAPIV3Schema struct { 41 | schema *spec.Schema 42 | } 43 | 44 | func (schema *openAPIV3Schema) GetMissingEntries(crInstance interface{}) []SchemaEntry { 45 | return getMissingEntries(schema.schema, crInstance) 46 | } 47 | 48 | func (schema *openAPIV3Schema) Validate(data interface{}) error { 49 | return validate.AgainstSchema(schema.schema, data, strfmt.Default) 50 | } 51 | 52 | type customResourceDefinition struct { 53 | Spec customResourceDefinitionSpec `json:"spec,omitempty"` 54 | } 55 | 56 | type customResourceDefinitionSpec struct { 57 | Versions []customResourceDefinitionVersion `json:"versions,omitempty"` 58 | Validation customResourceDefinitionValidation `json:"validation,omitempty"` 59 | } 60 | 61 | type customResourceDefinitionVersion struct { 62 | Name string `json:"Name,omitempty"` 63 | Schema customResourceDefinitionValidation `json:"schema,omitempty"` 64 | } 65 | 66 | type customResourceDefinitionValidation struct { 67 | OpenAPIV3Schema spec.Schema `json:"openAPIV3Schema,omitempty"` 68 | } 69 | -------------------------------------------------------------------------------- /pkg/validation/schema_sync.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "fmt" 5 | "github.com/go-openapi/spec" 6 | "reflect" 7 | "strings" 8 | ) 9 | 10 | type SchemaEntry struct { 11 | Path string 12 | Type string 13 | } 14 | 15 | func getMissingEntries(schema *spec.Schema, crInstance interface{}) []SchemaEntry { 16 | var entries []SchemaEntry 17 | crStruct := reflect.ValueOf(crInstance).Elem().Type() 18 | if field, found := crStruct.FieldByName("Spec"); found { 19 | entries = validateField(entries, *schema, "", field) 20 | } 21 | if field, found := crStruct.FieldByName("Status"); found { 22 | entries = validateField(entries, *schema, "", field) 23 | } 24 | return entries 25 | } 26 | 27 | func validateField(entries []SchemaEntry, schema spec.Schema, context string, field reflect.StructField) []SchemaEntry { 28 | reflectType := getActualType(field) 29 | if !field.Anonymous { 30 | name := getFieldName(field) 31 | context = fmt.Sprintf("%s/%s", context, name) 32 | schema = schema.Properties[name] 33 | expectedType := equivalentSchemaType(reflectType.Kind()) 34 | if !schema.Type.Contains(expectedType) { 35 | entries = append(entries, SchemaEntry{context, expectedType}) 36 | } 37 | } 38 | if isArray(reflectType) { 39 | reflectType = reflectType.Elem() 40 | if schema.Items != nil { 41 | schema = *schema.Items.Schema 42 | } 43 | } 44 | for _, field := range getChildren(field) { 45 | entries = validateField(entries, schema, context, field) 46 | } 47 | return entries 48 | } 49 | 50 | func getChildren(field reflect.StructField) []reflect.StructField { 51 | reflectType := getActualType(field) 52 | if reflectType.Kind() == reflect.Struct { 53 | return getFields(reflectType) 54 | } else if isArray(reflectType) { 55 | elem := reflectType.Elem() 56 | if elem.Kind() == reflect.Struct { 57 | return getFields(elem) 58 | } 59 | } 60 | return nil 61 | } 62 | 63 | func isArray(fieldType reflect.Type) bool { 64 | switch fieldType.Kind() { 65 | case reflect.Slice: 66 | return true 67 | case reflect.Array: 68 | return true 69 | default: 70 | return false 71 | } 72 | } 73 | 74 | func getFields(fieldType reflect.Type) []reflect.StructField { 75 | var children []reflect.StructField 76 | for index := 0; index < fieldType.NumField(); index++ { 77 | children = append(children, fieldType.Field(index)) 78 | } 79 | return children 80 | } 81 | 82 | func getActualType(field reflect.StructField) reflect.Type { 83 | reflectType := field.Type 84 | if reflectType.Kind() == reflect.Ptr { 85 | reflectType = reflectType.Elem() 86 | } 87 | return reflectType 88 | } 89 | 90 | func equivalentSchemaType(kind reflect.Kind) string { 91 | switch kind { 92 | case reflect.String: 93 | return "string" 94 | case reflect.Float32: 95 | return "number" 96 | case reflect.Float64: 97 | return "number" 98 | case reflect.Int: 99 | return "integer" 100 | case reflect.Int8: 101 | return "integer" 102 | case reflect.Int16: 103 | return "integer" 104 | case reflect.Int32: 105 | return "integer" 106 | case reflect.Int64: 107 | return "integer" 108 | case reflect.Bool: 109 | return "boolean" 110 | case reflect.Struct: 111 | return "object" 112 | case reflect.Ptr: 113 | return "object" 114 | case reflect.Map: 115 | return "object" 116 | case reflect.Array: 117 | return "array" 118 | case reflect.Slice: 119 | return "array" 120 | default: 121 | return "" 122 | } 123 | } 124 | 125 | func getFieldName(field reflect.StructField) string { 126 | tag := string(field.Tag) 127 | parts := strings.Split(tag, ":") 128 | if len(parts) == 1 || parts[0] != "json" { 129 | return field.Name 130 | } else { 131 | quotesRemoved := strings.Replace(parts[1], "\"", "", -1) 132 | commaDelimited := strings.Split(quotesRemoved, ",") 133 | spaceDelimited := strings.Split(commaDelimited[0], " ") 134 | return spaceDelimited[0] 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /pkg/validation/schema_sync_test.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | type sampleApp struct { 10 | Spec sampleAppSpec `json:"spec,omitempty"` 11 | Status sampleAppStatus `json:"status,omitempty"` 12 | } 13 | 14 | type sampleAppSpec struct { 15 | SimpleText string `json:"simpleText,omitempty"` 16 | secondAppObject `json:",inline"` 17 | IntPtr *int32 `json:"intPtr,omitempty"` 18 | ObjArray []env `json:"envArray,omitempty"` 19 | } 20 | 21 | type secondAppObject struct { 22 | OtherText string `json:"otherText,omitempty"` 23 | } 24 | 25 | type sampleAppStatus struct { 26 | StatusText string `json:"statusText,omitempty"` 27 | } 28 | 29 | type env struct { 30 | Name string `json:"name,omitempty"` 31 | Value string `json:"value,omitempty"` 32 | } 33 | 34 | func TestSchemaStructComplaince(t *testing.T) { 35 | schema := getCompleteSchema(t) 36 | missingEntries := schema.GetMissingEntries(&sampleApp{}) 37 | for _, missing := range missingEntries { 38 | if strings.HasPrefix(missing.Path, "/status") { 39 | //Not using subresources, so status is not expected to appear in CRD 40 | } else { 41 | assert.Fail(t, "Discrepancy between CRD and Struct", "Missing or incorrect schema validation at %v, expected type %v", missing.Path, missing.Type) 42 | } 43 | } 44 | } 45 | 46 | func TestSchemaStructInlineJson(t *testing.T) { 47 | schema := getSchemaWithoutInline(t) 48 | missingEntries := schema.GetMissingEntries(&sampleApp{}) 49 | assert.Len(t, missingEntries, 3, "Expect two status fields and one inline otherText field to be caught") 50 | for _, missing := range missingEntries { 51 | if strings.HasPrefix(missing.Path, "/status") { 52 | //Not using subresources, so status is not expected to appear in CRD 53 | } else { 54 | assert.Equal(t, "/spec/otherText", missing.Path, "Other than status fields, expected to find /spec/otherText but instead found %s", missing.Path) 55 | } 56 | } 57 | } 58 | 59 | func TestSchemaStructIntPointer(t *testing.T) { 60 | schema := getSchemaWithoutIntPointer(t) 61 | missingEntries := schema.GetMissingEntries(&sampleApp{}) 62 | assert.Len(t, missingEntries, 3, "Expect two status fields and one integer pointer field to be caught") 63 | for _, missing := range missingEntries { 64 | if strings.HasPrefix(missing.Path, "/status") { 65 | //Not using subresources, so status is not expected to appear in CRD 66 | } else { 67 | assert.Equal(t, "/spec/intPtr", missing.Path, "Other than status fields, expected to find /spec/intPtr but instead found %s", missing.Path) 68 | } 69 | } 70 | } 71 | 72 | func TestSchemaStructSlice(t *testing.T) { 73 | schema := getSchemaWithoutSliceTypes(t) 74 | missingEntries := schema.GetMissingEntries(&sampleApp{}) 75 | assert.Len(t, missingEntries, 4, "Expect two status fields and two sub-types of the slice to be caught") 76 | for _, missing := range missingEntries { 77 | if strings.HasPrefix(missing.Path, "/status") { 78 | //Not using subresources, so status is not expected to appear in CRD 79 | } else if missing.Path == "/spec/envArray/name" { 80 | //Expected 81 | } else if missing.Path == "/spec/envArray/value" { 82 | //Expected 83 | } else { 84 | assert.Fail(t, "Unexpected validation failure", "Did not expect to fail with %s of type %s", missing.Path, missing.Type) 85 | } 86 | } 87 | } 88 | 89 | func TestSchemaFloat64(t *testing.T) { 90 | schemaYaml := ` 91 | apiVersion: apiextensions.k8s.io/v1beta1 92 | kind: CustomResourceDefinition 93 | metadata: 94 | name: sample.app.example.com 95 | spec: 96 | group: app.example.com 97 | names: 98 | kind: SampleApp 99 | listKind: SampleAppList 100 | plural: sampleapps 101 | singular: sampleapp 102 | scope: Namespaced 103 | version: v1 104 | validation: 105 | openAPIV3Schema: 106 | required: 107 | - spec 108 | properties: 109 | spec: 110 | type: object 111 | required: 112 | - number 113 | properties: 114 | number: 115 | type: number 116 | format: double 117 | ` 118 | schema, err := New([]byte(schemaYaml)) 119 | assert.NoError(t, err) 120 | 121 | type myAppSpec struct { 122 | Number float64 `json:"number,omitempty"` 123 | } 124 | 125 | type myApp struct { 126 | Spec myAppSpec `json:"spec,omitempty"` 127 | } 128 | 129 | cr := myApp{ 130 | Spec: myAppSpec{ 131 | Number: float64(23), 132 | }, 133 | } 134 | missingEntries := schema.GetMissingEntries(&cr) 135 | assert.Len(t, missingEntries, 0, "Expect no missing entries in CRD for this struct: %v", missingEntries) 136 | } 137 | 138 | func getCompleteSchema(t *testing.T) Schema { 139 | schemaYaml := ` 140 | apiVersion: apiextensions.k8s.io/v1beta1 141 | kind: CustomResourceDefinition 142 | metadata: 143 | name: sample.app.example.com 144 | spec: 145 | group: app.example.com 146 | names: 147 | kind: SampleApp 148 | listKind: SampleAppList 149 | plural: sampleapps 150 | singular: sampleapp 151 | scope: Namespaced 152 | version: v1 153 | validation: 154 | openAPIV3Schema: 155 | required: 156 | - spec 157 | properties: 158 | spec: 159 | type: object 160 | properties: 161 | simpleText: 162 | type: string 163 | otherText: 164 | type: string 165 | intPtr: 166 | type: integer 167 | simpleObject: 168 | type: object 169 | properties: 170 | simpleField: 171 | type: string 172 | envArray: 173 | type: array 174 | items: 175 | type: object 176 | properties: 177 | name: 178 | type: string 179 | value: 180 | type: string 181 | ` 182 | schema, err := New([]byte(schemaYaml)) 183 | assert.NoError(t, err) 184 | return schema 185 | } 186 | 187 | func getSchemaWithoutInline(t *testing.T) Schema { 188 | schemaYaml := ` 189 | apiVersion: apiextensions.k8s.io/v1beta1 190 | kind: CustomResourceDefinition 191 | metadata: 192 | name: sample.app.example.com 193 | spec: 194 | group: app.example.com 195 | names: 196 | kind: SampleApp 197 | listKind: SampleAppList 198 | plural: sampleapps 199 | singular: sampleapp 200 | scope: Namespaced 201 | version: v1 202 | validation: 203 | openAPIV3Schema: 204 | required: 205 | - spec 206 | properties: 207 | spec: 208 | type: object 209 | properties: 210 | simpleText: 211 | type: string 212 | intPtr: 213 | type: integer 214 | simpleObject: 215 | type: object 216 | properties: 217 | simpleField: 218 | type: string 219 | envArray: 220 | type: array 221 | items: 222 | type: object 223 | properties: 224 | name: 225 | type: string 226 | value: 227 | type: string 228 | ` 229 | schema, err := New([]byte(schemaYaml)) 230 | assert.NoError(t, err) 231 | return schema 232 | } 233 | 234 | func getSchemaWithoutIntPointer(t *testing.T) Schema { 235 | schemaYaml := ` 236 | apiVersion: apiextensions.k8s.io/v1beta1 237 | kind: CustomResourceDefinition 238 | metadata: 239 | name: sample.app.example.com 240 | spec: 241 | group: app.example.com 242 | names: 243 | kind: SampleApp 244 | listKind: SampleAppList 245 | plural: sampleapps 246 | singular: sampleapp 247 | scope: Namespaced 248 | version: v1 249 | validation: 250 | openAPIV3Schema: 251 | required: 252 | - spec 253 | properties: 254 | spec: 255 | type: object 256 | properties: 257 | simpleText: 258 | type: string 259 | otherText: 260 | type: string 261 | simpleObject: 262 | type: object 263 | properties: 264 | simpleField: 265 | type: string 266 | envArray: 267 | type: array 268 | items: 269 | type: object 270 | properties: 271 | name: 272 | type: string 273 | value: 274 | type: string 275 | ` 276 | schema, err := New([]byte(schemaYaml)) 277 | assert.NoError(t, err) 278 | return schema 279 | } 280 | 281 | func getSchemaWithoutSliceTypes(t *testing.T) Schema { 282 | schemaYaml := ` 283 | apiVersion: apiextensions.k8s.io/v1beta1 284 | kind: CustomResourceDefinition 285 | metadata: 286 | name: sample.app.example.com 287 | spec: 288 | group: app.example.com 289 | names: 290 | kind: SampleApp 291 | listKind: SampleAppList 292 | plural: sampleapps 293 | singular: sampleapp 294 | scope: Namespaced 295 | version: v1 296 | validation: 297 | openAPIV3Schema: 298 | required: 299 | - spec 300 | properties: 301 | spec: 302 | type: object 303 | properties: 304 | simpleText: 305 | type: string 306 | otherText: 307 | type: string 308 | intPtr: 309 | type: integer 310 | simpleObject: 311 | type: object 312 | properties: 313 | simpleField: 314 | type: string 315 | envArray: 316 | type: array 317 | ` 318 | schema, err := New([]byte(schemaYaml)) 319 | assert.NoError(t, err) 320 | return schema 321 | } 322 | -------------------------------------------------------------------------------- /pkg/validation/schema_test.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/ghodss/yaml" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestValidSample(t *testing.T) { 12 | var inputYaml = ` 13 | apiVersion: app.example.com/v1 14 | kind: SampleApp 15 | metadata: 16 | name: test 17 | spec: 18 | simpleText: value1 19 | simpleObject: 20 | simpleField: value2 21 | ` 22 | var input map[string]interface{} 23 | assert.NoError(t, yaml.Unmarshal([]byte(inputYaml), &input)) 24 | 25 | schema := getSampleSchema(t) 26 | assert.NoError(t, schema.Validate(input)) 27 | } 28 | 29 | func TestValidSubset(t *testing.T) { 30 | var inputYaml = ` 31 | apiVersion: app.example.com/v1 32 | kind: SampleApp 33 | metadata: 34 | name: test 35 | spec: 36 | simpleText: value 37 | ` 38 | var input map[string]interface{} 39 | assert.NoError(t, yaml.Unmarshal([]byte(inputYaml), &input)) 40 | 41 | schema := getSampleSchema(t) 42 | assert.NoError(t, schema.Validate(input)) 43 | } 44 | 45 | func TestValidSuperset(t *testing.T) { 46 | var inputYaml = ` 47 | apiVersion: app.example.com/v1 48 | kind: SampleApp 49 | metadata: 50 | name: test 51 | spec: 52 | simpleText: value1 53 | simpleObject: 54 | simpleField: value2 55 | simpleField2: value3 56 | simpleText2: value4 57 | ` 58 | var input map[string]interface{} 59 | assert.NoError(t, yaml.Unmarshal([]byte(inputYaml), &input)) 60 | 61 | schema := getSampleSchema(t) 62 | assert.NoError(t, schema.Validate(input)) 63 | } 64 | 65 | func TestInValidSample(t *testing.T) { 66 | var inputYaml = ` 67 | apiVersion: app.example.com/v1 68 | kind: SampleApp 69 | metadata: 70 | name: test 71 | spec: 72 | simpleText: value1 73 | simpleObject: value2 74 | ` 75 | var input map[string]interface{} 76 | assert.NoError(t, yaml.Unmarshal([]byte(inputYaml), &input)) 77 | 78 | schema := getSampleSchema(t) 79 | assert.Error(t, schema.Validate(input)) 80 | } 81 | 82 | func TestValidVersionedSuperset(t *testing.T) { 83 | var inputYaml = ` 84 | apiVersion: app.example.com/v1 85 | kind: SampleApp 86 | metadata: 87 | name: test 88 | spec: 89 | simpleText: value1 90 | simpleObject: 91 | simpleField: value2 92 | simpleField2: value3 93 | simpleText2: value4 94 | ` 95 | var input map[string]interface{} 96 | assert.NoError(t, yaml.Unmarshal([]byte(inputYaml), &input)) 97 | 98 | schema := getSampleVersionedSchema(t, "v1") 99 | assert.NoError(t, schema.Validate(input)) 100 | } 101 | 102 | func TestInValidVersionedSample(t *testing.T) { 103 | var inputYaml = ` 104 | apiVersion: app.example.com/v1beta1 105 | kind: SampleApp 106 | metadata: 107 | name: test 108 | spec: 109 | simpleText: value1 110 | simpleObject: 111 | simpleField: value2 112 | simpleField2: value3 113 | simpleText2: value4 114 | ` 115 | var input map[string]interface{} 116 | assert.NoError(t, yaml.Unmarshal([]byte(inputYaml), &input)) 117 | 118 | schema := getSampleVersionedSchema(t, "v1beta1") 119 | assert.Error(t, schema.Validate(input)) 120 | } 121 | 122 | func TestMissingVersion(t *testing.T) { 123 | schema := getSampleVersionedSchema(t, "v1alpha1") 124 | assert.Empty(t, schema) 125 | } 126 | 127 | func getSampleSchema(t *testing.T) Schema { 128 | schemaYaml := ` 129 | apiVersion: apiextensions.k8s.io/v1beta1 130 | kind: CustomResourceDefinition 131 | metadata: 132 | name: sample.app.example.com 133 | spec: 134 | group: app.example.com 135 | names: 136 | kind: SampleApp 137 | listKind: SampleAppList 138 | plural: sampleapps 139 | singular: sampleapp 140 | scope: Namespaced 141 | version: v1 142 | validation: 143 | openAPIV3Schema: 144 | required: 145 | - spec 146 | properties: 147 | spec: 148 | type: object 149 | properties: 150 | simpleText: 151 | type: string 152 | simpleObject: 153 | type: object 154 | properties: 155 | simpleField: 156 | type: string 157 | ` 158 | schema, err := New([]byte(schemaYaml)) 159 | assert.NoError(t, err) 160 | return schema 161 | } 162 | 163 | func getSampleVersionedSchema(t *testing.T, version string) Schema { 164 | schemaYaml := ` 165 | apiVersion: apiextensions.k8s.io/v1beta1 166 | kind: CustomResourceDefinition 167 | metadata: 168 | name: sample.app.example.com 169 | spec: 170 | group: app.example.com 171 | names: 172 | kind: SampleApp 173 | listKind: SampleAppList 174 | plural: sampleapps 175 | singular: sampleapp 176 | scope: Namespaced 177 | versions: 178 | - name: v1 179 | schema: 180 | openAPIV3Schema: 181 | required: 182 | - spec 183 | properties: 184 | spec: 185 | type: object 186 | properties: 187 | simpleText: 188 | type: string 189 | simpleObject: 190 | type: object 191 | properties: 192 | simpleField: 193 | type: string 194 | - name: v1beta1 195 | schema: 196 | openAPIV3Schema: 197 | required: 198 | - spec 199 | properties: 200 | spec: 201 | type: object 202 | properties: 203 | simpleText: 204 | type: string 205 | simpleObject: 206 | type: object 207 | properties: 208 | simpleField: 209 | type: integer 210 | ` 211 | 212 | schema, err := NewVersioned([]byte(schemaYaml), version) 213 | if err != nil { 214 | assert.EqualError(t, err, fmt.Sprintf("no version %s detected in crd", version)) 215 | } 216 | return schema 217 | } 218 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | var ( 4 | Version = "0.1" 5 | ) 6 | --------------------------------------------------------------------------------