├── .dockerignore ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .goreleaser.dev.yml ├── .goreleaser.yml ├── Dockerfile ├── Iris.jpg ├── LICENSE ├── Makefile ├── README.md ├── VERSION ├── cmd ├── cmdutils.go ├── root.go └── run.go ├── go.mod ├── go.sum ├── hack ├── .codefresh │ └── push-helm.sh ├── build.sh └── test.sh ├── iris ├── Chart.yaml ├── templates │ ├── _helpers.tpl │ ├── cm.yaml │ ├── deploy.yaml │ ├── namespace.yaml │ └── role.yaml └── values.yaml ├── main.go └── pkg ├── app └── app.go ├── dal ├── dal.go └── integration.go ├── destination ├── codefresh_destination.go ├── default_destination.go ├── destination.go └── service.go ├── filter ├── any_filter.go ├── factory.go ├── factory_internal_test.go ├── factory_test.go ├── filter.go ├── filter_apply_test.go ├── filter_test.go ├── helpers_test.go ├── jsonpath_filter.go ├── label_filter.go ├── mocks │ ├── Factory.go │ ├── Filter.go │ └── Service.go ├── namespace_filter.go ├── reason_filter.go ├── service.go └── service_test.go ├── kube ├── kube.go └── mocks │ └── Kube.go ├── logger └── logger.go ├── reader ├── reader.go └── reader_test.go ├── server └── server.go └── util ├── reader └── file │ ├── file.go │ └── file_test.go └── util.go /.dockerignore: -------------------------------------------------------------------------------- 1 | example.* 2 | Dockerfile 3 | kube-resourecs 4 | .envrc 5 | .vscode 6 | .cover 7 | .examples -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [ '*' ] 6 | 7 | jobs: 8 | ci: 9 | name: ci 10 | runs-on: "ubuntu-18.04" 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-go@v2 14 | with: 15 | go-version: '^1.18' 16 | - run: make test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | example.* 3 | kube-resources 4 | .envrc 5 | .vscode 6 | .cover 7 | .examples 8 | vendor -------------------------------------------------------------------------------- /.goreleaser.dev.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - env: 3 | - CGO_ENABLED=0 4 | goos: 5 | - darwin 6 | - linux 7 | ldflags: 8 | - -s -w 9 | - -X 'github.com/olegsu/iris/pkg/util.BuildVersion={{.Version}}' 10 | - -X 'github.com/olegsu/iris/pkg/util.BuildDate={{.Date}}' 11 | - -X 'github.com/olegsu/iris/pkg/util.BuildCommit={{.ShortCommit}}' 12 | - -X 'github.com/olegsu/iris/pkg/util.BuildBy=goreleaser' 13 | archive: 14 | replacements: 15 | darwin: Darwin 16 | linux: LinuxS 17 | checksum: 18 | name_template: 'checksums.txt' 19 | snapshot: 20 | name_template: "{{ .Tag }}-next" 21 | changelog: 22 | sort: asc 23 | filters: 24 | exclude: 25 | - '^docs:' 26 | - '^test:' 27 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - env: 3 | - CGO_ENABLED=0 4 | goos: 5 | - darwin 6 | - linux 7 | - windows 8 | ldflags: 9 | - -s -w 10 | - -X 'github.com/olegsu/iris/pkg/util.BuildVersion={{.Version}}' 11 | - -X 'github.com/olegsu/iris/pkg/util.BuildDate={{.Date}}' 12 | - -X 'github.com/olegsu/iris/pkg/util.BuildCommit={{.ShortCommit}}' 13 | - -X 'github.com/olegsu/iris/pkg/util.BuildBy=goreleaser' 14 | 15 | archive: 16 | replacements: 17 | darwin: Darwin 18 | linux: Linux 19 | windows: Windows 20 | 386: i386 21 | amd64: x86_64 22 | format_overrides: 23 | - goos: windows 24 | format: zip 25 | checksum: 26 | name_template: 'checksums.txt' 27 | snapshot: 28 | name_template: "{{ .Tag }}-next" 29 | changelog: 30 | sort: asc 31 | filters: 32 | exclude: 33 | - '^docs:' 34 | - '^test:' 35 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.8 2 | 3 | RUN apk --no-cache add --update ca-certificates 4 | 5 | COPY dist/iris_linux_386/iris /usr/local/bin/ 6 | 7 | ENTRYPOINT ["/usr/local/bin/iris"] 8 | CMD [ "--help" ] 9 | -------------------------------------------------------------------------------- /Iris.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olegsu/iris/2c8c0a1f81500e6454f8a7cbd1a46709801a52a8/Iris.jpg -------------------------------------------------------------------------------- /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 | test: 2 | ./hack/test.sh 3 | 4 | build-local: 5 | sh ./hack/build.sh -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IRIS 2 | 3 | 4 | 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/olegsu/iris)](https://goreportcard.com/report/github.com/olegsu/iris) 6 | [![codecov](https://codecov.io/gh/olegsu/iris/branch/master/graph/badge.svg)](https://codecov.io/gh/olegsu/iris) 7 | 8 | **_In Greek mythology, Iris is the personification of the rainbow and messenger of the gods._** 9 | 10 | - [IRIS](#iris) 11 | - [Run in cluster](#run-in-cluster) 12 | - [Using Helm](#using-helm) 13 | - [Build](#build) 14 | - [Filters](#filters) 15 | - [Reason](#reason) 16 | - [Namespace](#namespace) 17 | - [JSONPath](#jsonpath) 18 | - [Labels](#labels) 19 | - [Any](#any) 20 | - [Destinations](#destinations) 21 | - [Default](#default) 22 | - [Codefresh](#codefresh) 23 | - [Integrations](#integrations) 24 | 25 | Easily configure webhooks on Kubernetes events using highly customizable filters 26 | 27 | * This project is not stable yet and may be changed anytime without any notice. 28 | 29 | ## Run in cluster 30 | ### Using Helm 31 | * clone or fork this repository 32 | * create your iris.yaml file 33 | * install chart from local directory `helm install ./iris --values ./iris.yaml` 34 | * by default the chart will be installed into namespace `iris`, see default values to overwrite it 35 | 36 | ## Build 37 | * clone or fork this repo 38 | * `make install` 39 | * `make build` 40 | * Limitations: 41 | * Execute out of cluster `iris run --help` 42 | * Execute on non GCP cluster 43 | 44 | Quick example: 45 | 46 | In this example we will configure to listen on any Kubernetes event that been reported by the pod controller and matched to the filter will be sent to the destination. 47 | 48 | ```yaml 49 | filters: 50 | - name: MatchDefaultNamespace 51 | type: namespace 52 | namespace: default 53 | - name: MatchPodKind 54 | type: jsonpath 55 | path: $.involvedObject.kind 56 | value: Pod 57 | 58 | destinations: 59 | - name: prod 60 | url: http://localhost 61 | 62 | integrations: 63 | - name: Report 64 | destinations: 65 | - prod 66 | filters: 67 | - MatchPodKind 68 | - MatchDefaultNamespace 69 | ``` 70 | 71 | ## Filters 72 | Set of rules that will be applied on each [Kubernetes event](https://github.com/kubernetes/api/blob/master/core/v1/types.go#L4501). 73 | Kubernetes event that will pass all required filters will be passed to the destination to be reported 74 | Types of filters: 75 | ### Reason 76 | Reason filter is a syntactic sugar for [JSONPath](#jsonpath) filter with `path: $.reason` and `value: {{reason}}` 77 | ```yaml 78 | filters: 79 | - name: PodScheduled 80 | reason: "Scheduled" 81 | ``` 82 | 83 | ### Namespace 84 | Namespace filter is a syntactic sugar for [JSONPath](#jsonpath) filter with `path: $.metadata.namespace` and `value: {{reason}}` 85 | ```yaml 86 | filters: 87 | - name: FromDefaultNamespace 88 | namespace: default 89 | ``` 90 | 91 | ### JSONPath 92 | With JSONPath gives the ability to match any field from [Kubernetes event](https://github.com/kubernetes/api/blob/master/core/v1/types.go#L5478). 93 | The value from the fields can be matched to exec value using `value: {{value}}` or matched by regex using `regexp: {{regexp}}` 94 | ```yaml 95 | filters: 96 | # Match to Warning event type 97 | - name: WarningLevel 98 | type: jsonpath 99 | path: $.type 100 | value: Warning 101 | # Match to any event that the name matched to regexp /tiller/ 102 | - name: MatchToRegexpTiller 103 | type: jsonpath 104 | path: $.metadata.name 105 | regexp: tiller 106 | ``` 107 | 108 | 109 | 110 | ### Labels 111 | Labels filter will try to get the original resource from the event with the given filters. 112 | The filter considers as passed if any resource were found 113 | ```yaml 114 | filters: 115 | - name: MatchLabels 116 | type: labels 117 | labels: 118 | app: helm 119 | ``` 120 | 121 | ### Any 122 | ```yaml 123 | filters: 124 | - name: WarningLevel 125 | type: any 126 | filters: 127 | - FromDefaultNamespace 128 | - WarningLevel 129 | ``` 130 | 131 | ## Destinations 132 | Each destination is an api endpoint where the Kubernetes event will be sent 133 | Types of destinations: 134 | ### Default 135 | The default destinations will attempt to send POST request with the event json in the request body 136 | If `secret` is given, hash string will be calculated using the given key on the request body and the result will be set in the request header as `X-IRIS-HMAC: hash` 137 | ```yaml 138 | destinations: 139 | - name: Webhook 140 | url: https://webhook.site 141 | secret: SECRET 142 | ``` 143 | ### Codefresh 144 | With Iris, you can execute Codefresh pipelines. 145 | Add destinations with Codefresh type: 146 | * name: pipeline full name can be found easily using [Codefresh CLI](https://codefresh-io.github.io/cli/) - `codefresh get pipelines` 147 | * branch: which branch of the repo should be cloned 148 | * cftoken: Token to Codefresh API can be generated in [Account settings/Tokens](https://g.codefresh.io/account-conf/tokens) view 149 | ```yaml 150 | - name: ExecuteCodefreshPipeline 151 | type: Codefresh 152 | pipeline: PIPELINE_NAME 153 | cftoken: API_TOKEN 154 | branch: master 155 | ``` 156 | 157 | ## Integrations 158 | Connecting between filters and destinations 159 | ```yaml 160 | integrations: 161 | - name: Report 162 | destinations: 163 | - {{name of destination}} 164 | filters: 165 | - {{name of filters to apply}} 166 | ``` 167 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.7.0 2 | -------------------------------------------------------------------------------- /cmd/cmdutils.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | // Copyright © 2019 oleg2807@gmail.com 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | 21 | "github.com/olegsu/iris/pkg/logger" 22 | ) 23 | 24 | func dieOnError(err error) { 25 | if err != nil { 26 | fmt.Errorf(err.Error()) 27 | os.Exit(1) 28 | } 29 | } 30 | 31 | func buildLogger(cmd string) logger.Logger { 32 | return logger.New(&logger.Options{ 33 | Command: cmd, 34 | Verbose: true, 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | // Copyright © 2019 oleg2807@gmail.com 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import ( 18 | "github.com/olegsu/iris/pkg/util" 19 | 20 | "github.com/spf13/cobra" 21 | ) 22 | 23 | var rootCmdOptions struct { 24 | Verbose bool 25 | } 26 | 27 | var rootCmd = &cobra.Command{ 28 | Use: "iris", 29 | Long: "Watch on Kubernetes events, filter and send them as standard wehbook to any system", 30 | Version: util.BuildVersion, 31 | } 32 | 33 | // Execute - execute the root command 34 | func Execute() { 35 | err := rootCmd.Execute() 36 | dieOnError(err) 37 | } 38 | 39 | func init() { 40 | rootCmd.PersistentFlags().BoolVar(&rootCmdOptions.Verbose, "verbose", false, "Set to get more detailed output") 41 | } 42 | -------------------------------------------------------------------------------- /cmd/run.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | // Copyright © 2019 oleg2807@gmail.com 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | 21 | "github.com/spf13/cobra" 22 | "github.com/spf13/viper" 23 | 24 | "github.com/olegsu/iris/pkg/app" 25 | ) 26 | 27 | var runCmdOptions struct { 28 | file string 29 | kube string 30 | inCluster bool 31 | } 32 | 33 | var runCmd = &cobra.Command{ 34 | Use: "run", 35 | Long: "Start the server", 36 | Run: func(cmd *cobra.Command, args []string) { 37 | logger := buildLogger("run") 38 | if runCmdOptions.kube == "" { 39 | path := fmt.Sprintf("%s/.kube/config", os.Getenv("HOME")) 40 | logger.Debug("Path to kubeconfig not set, using default", "path", path) 41 | runCmdOptions.kube = path 42 | } 43 | config := app.NewApplicationOptions(runCmdOptions.file, runCmdOptions.kube, runCmdOptions.inCluster, logger) 44 | app.CreateApp(config) 45 | }, 46 | } 47 | 48 | func init() { 49 | rootCmd.AddCommand(runCmd) 50 | 51 | viper.BindEnv("file", "IRIS_FILE") 52 | 53 | viper.BindEnv("kube", "KUBECONFIG") 54 | 55 | viper.BindEnv("inCluster", "IRIS_IN_CLUSTER") 56 | 57 | runCmd.Flags().StringVar(&runCmdOptions.file, "iris-file", viper.GetString("file"), "Iris yaml config file [$IRIS_FILE]") 58 | runCmd.Flags().StringVar(&runCmdOptions.kube, "kube-config", viper.GetString("kube"), "Path to kube-config file (default is ~/.kube/config) [$KUBECONFIG]") 59 | runCmd.Flags().BoolVar(&runCmdOptions.inCluster, "in-cluster", viper.GetBool("inCluster"), "Set when running inside a cluster. NOTE: This option will ignore --kube-config flag [$IRIS_IN_CLUSTER]") 60 | 61 | } 62 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/olegsu/iris 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/gin-gonic/gin v0.0.0-20170702092826-d459835d2b07 7 | github.com/inconshreveable/log15 v0.0.0-20180818164646-67afb5ed74ec 8 | github.com/spf13/cobra v0.0.3 9 | github.com/spf13/viper v1.3.1 10 | github.com/stretchr/testify v1.3.0 11 | github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 12 | gopkg.in/yaml.v2 v2.2.8 13 | k8s.io/api v0.0.0-20180628040859-072894a440bd 14 | k8s.io/apimachinery v0.0.0-20180621070125-103fd098999d 15 | k8s.io/client-go v8.0.0+incompatible 16 | ) 17 | 18 | require ( 19 | github.com/BurntSushi/toml v0.3.1 // indirect 20 | github.com/davecgh/go-spew v1.1.1 // indirect 21 | github.com/fsnotify/fsnotify v1.4.7 // indirect 22 | github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680 // indirect 23 | github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 // indirect 24 | github.com/go-stack/stack v1.8.0 // indirect 25 | github.com/gogo/protobuf v0.0.0-20170330071051-c0656edd0d9e // indirect 26 | github.com/golang/glog v0.0.0-20141105023935-44145f04b68c // indirect 27 | github.com/golang/groupcache v0.0.0-20181024230925-c65c006176ff // indirect 28 | github.com/golang/protobuf v1.1.0 // indirect 29 | github.com/google/btree v0.0.0-20160524151835-7d79101e329e // indirect 30 | github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367 // indirect 31 | github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d // indirect 32 | github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7 // indirect 33 | github.com/hashicorp/golang-lru v0.0.0-20160207214719-a0d98a5f2880 // indirect 34 | github.com/hashicorp/hcl v1.0.0 // indirect 35 | github.com/imdario/mergo v0.0.0-20141206190957-6633656539c1 // indirect 36 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 37 | github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3 // indirect 38 | github.com/magiconair/properties v1.8.0 // indirect 39 | github.com/mattn/go-colorable v0.1.1 // indirect 40 | github.com/mattn/go-isatty v0.0.5 // indirect 41 | github.com/mitchellh/mapstructure v1.1.2 // indirect 42 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 43 | github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da // indirect 44 | github.com/pelletier/go-toml v1.2.0 // indirect 45 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 46 | github.com/pmezard/go-difflib v1.0.0 // indirect 47 | github.com/spf13/afero v1.1.2 // indirect 48 | github.com/spf13/cast v1.3.0 // indirect 49 | github.com/spf13/jwalterweatherman v1.0.0 // indirect 50 | github.com/spf13/pflag v1.0.3 // indirect 51 | github.com/stretchr/objx v0.1.0 // indirect 52 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 // indirect 53 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 // indirect 54 | golang.org/x/net v0.0.0-20170809000501-1c05540f6879 // indirect 55 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect 56 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 // indirect 57 | golang.org/x/text v0.3.0 // indirect 58 | golang.org/x/time v0.0.0-20161028155119-f51c12702a4d // indirect 59 | gopkg.in/go-playground/assert.v1 v1.2.1 // indirect 60 | gopkg.in/go-playground/validator.v8 v8.18.2 // indirect 61 | gopkg.in/inf.v0 v0.9.0 // indirect 62 | gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 // indirect 63 | ) 64 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 4 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 5 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 6 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 11 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 12 | github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680 h1:ZktWZesgun21uEDrwW7iEV1zPCGQldM2atlJZ3TdvVM= 13 | github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 14 | github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY= 15 | github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= 16 | github.com/gin-gonic/gin v0.0.0-20170702092826-d459835d2b07 h1:cZPJWzd2oNeoS0oJM2TlN9rl0OnCgUr10gC8Q4mH+6M= 17 | github.com/gin-gonic/gin v0.0.0-20170702092826-d459835d2b07/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= 18 | github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= 19 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 20 | github.com/gogo/protobuf v0.0.0-20170330071051-c0656edd0d9e h1:ago6fNuQ6IhszPsXkeU7qRCyfsIX7L67WDybsAPkLl8= 21 | github.com/gogo/protobuf v0.0.0-20170330071051-c0656edd0d9e/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 22 | github.com/golang/glog v0.0.0-20141105023935-44145f04b68c h1:CbdkBQ1/PiAo0FYJhQGwASD8wrgNvTdf01g6+O9tNuA= 23 | github.com/golang/glog v0.0.0-20141105023935-44145f04b68c/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 24 | github.com/golang/groupcache v0.0.0-20181024230925-c65c006176ff h1:kOkM9whyQYodu09SJ6W3NCsHG7crFaJILQ22Gozp3lg= 25 | github.com/golang/groupcache v0.0.0-20181024230925-c65c006176ff/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 26 | github.com/golang/protobuf v1.1.0 h1:0iH4Ffd/meGoXqF2lSAhZHt8X+cPgkfn/cb6Cce5Vpc= 27 | github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 28 | github.com/google/btree v0.0.0-20160524151835-7d79101e329e h1:JHB7F/4TJCrYBW8+GZO8VkWDj1jxcWuCl6uxKODiyi4= 29 | github.com/google/btree v0.0.0-20160524151835-7d79101e329e/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 30 | github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367 h1:ScAXWS+TR6MZKex+7Z8rneuSJH+FSDqd6ocQyl+ZHo4= 31 | github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= 32 | github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k= 33 | github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= 34 | github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7 h1:6TSoaYExHper8PYsJu23GWVNOyYRCSnIFyxKgLSZ54w= 35 | github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 36 | github.com/hashicorp/golang-lru v0.0.0-20160207214719-a0d98a5f2880 h1:OaRuzt9oCKNui8cCskZijoKUwe+aCuuCwvx1ox8FNyw= 37 | github.com/hashicorp/golang-lru v0.0.0-20160207214719-a0d98a5f2880/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 38 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 39 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 40 | github.com/imdario/mergo v0.0.0-20141206190957-6633656539c1 h1:FeeCi0I2Fu8kA8IXrdVPtGzym+mW9bzfj9f26EaES9k= 41 | github.com/imdario/mergo v0.0.0-20141206190957-6633656539c1/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 42 | github.com/inconshreveable/log15 v0.0.0-20180818164646-67afb5ed74ec h1:CGkYB1Q7DSsH/ku+to+foV4agt2F2miquaLUgF6L178= 43 | github.com/inconshreveable/log15 v0.0.0-20180818164646-67afb5ed74ec/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o= 44 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 45 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 46 | github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3 h1:/UewZcckqhvnnS0C6r3Sher2hSEbVmM6Ogpcjen08+Y= 47 | github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 48 | github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= 49 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 50 | github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= 51 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 52 | github.com/mattn/go-isatty v0.0.5 h1:tHXDdz1cpzGaovsTB+TVB8q90WEokoVmfMqoVcrLUgw= 53 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 54 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 55 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 56 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 57 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 58 | github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da h1:ZQGIPjr1iTtUPXZFk8WShqb5G+Qg65VHFLtSvmHh+Mw= 59 | github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 60 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 61 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 62 | github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= 63 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 64 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 65 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 66 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 67 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 68 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 69 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 70 | github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= 71 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 72 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 73 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 74 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 75 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 76 | github.com/spf13/viper v1.3.1 h1:5+8j8FTpnFV4nEImW/ofkzEt8VoOiLXxdYIDsB73T38= 77 | github.com/spf13/viper v1.3.1/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 78 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 79 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 80 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 81 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 82 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 83 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 h1:3SVOIvH7Ae1KRYyQWRjXWJEA9sS/c/pjvH++55Gr648= 84 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 85 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 86 | github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= 87 | github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= 88 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0= 89 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 90 | golang.org/x/net v0.0.0-20170809000501-1c05540f6879 h1:0rFa7EaCGdQPmZVbo9F7MNF65b8dyzS6EUnXjs9Cllk= 91 | golang.org/x/net v0.0.0-20170809000501-1c05540f6879/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 92 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= 93 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 94 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 95 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= 96 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 97 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 98 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 99 | golang.org/x/time v0.0.0-20161028155119-f51c12702a4d h1:TnM+PKb3ylGmZvyPXmo9m/wktg7Jn/a/fNmr33HSj8g= 100 | golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 101 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 102 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 103 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 104 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 105 | gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= 106 | gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= 107 | gopkg.in/inf.v0 v0.9.0 h1:3zYtXIO92bvsdS3ggAdA8Gb4Azj0YU+TVY1uGYNFA8o= 108 | gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 109 | gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU= 110 | gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= 111 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 112 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 113 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 114 | k8s.io/api v0.0.0-20180628040859-072894a440bd h1:HzgYeLDS1jLxw8DGr68KJh9cdQ5iZJizG0HZWstIhfQ= 115 | k8s.io/api v0.0.0-20180628040859-072894a440bd/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA= 116 | k8s.io/apimachinery v0.0.0-20180621070125-103fd098999d h1:MZjlsu9igBoVPZkXpIGoxI6EonqNsXXZU7hhvfQLkd4= 117 | k8s.io/apimachinery v0.0.0-20180621070125-103fd098999d/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0= 118 | k8s.io/client-go v8.0.0+incompatible h1:tTI4hRmb1DRMl4fG6Vclfdi6nTM82oIrTT7HfitmxC4= 119 | k8s.io/client-go v8.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= 120 | -------------------------------------------------------------------------------- /hack/.codefresh/push-helm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | defaultBranch="master" 4 | 5 | getChartVersion(){ 6 | version="$(cat VERSION)" 7 | if [ "$CF_BRANCH_TAG_NORMALIZED" = "$defaultBranch" ] 8 | then 9 | version=$version 10 | else 11 | version=$version-$CF_BRANCH_TAG_NORMALIZED-$CF_SHORT_REVISION 12 | fi 13 | echo $version 14 | } 15 | 16 | updateChartVersion(){ 17 | version=$1 18 | yq --arg version "$version" '.version = $version' $CF_VOLUME_PATH/iris/iris/Chart.yaml | yq -y '.sources[.sources | length] = env.CF_COMMIT_URL' --yaml-output > $CF_VOLUME_PATH/Chart.new.yaml 19 | mv $CF_VOLUME_PATH/Chart.new.yaml $CF_VOLUME_PATH/iris/iris/Chart.yaml 20 | } 21 | 22 | v=$(getChartVersion) 23 | echo "Setting version to be $v" 24 | updateChartVersion $v 25 | cf_export IRIS_VERSION=$v -------------------------------------------------------------------------------- /hack/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | OUTFILE=${1:-/usr/local/bin/irisctl} 5 | BUILD_DATE=$(date) 6 | GIT_TAG=$(git describe --tags) 7 | GIT_COMMIT=$(git rev-parse --short HEAD) 8 | 9 | LDFLAGS="-s -w 10 | -X 'github.com/olegsu/iris/pkg/util.BuildVersion=${GIT_TAG}' 11 | -X 'github.com/olegsu/iris/pkg/util.BuildDate=${BUILD_DATE}' 12 | -X 'github.com/olegsu/iris/pkg/util.BuildCommit=${GIT_COMMIT}' 13 | -X 'github.com/olegsu/iris/pkg/util.BuildBy=Makefile' 14 | " 15 | 16 | go build -ldflags "$LDFLAGS" -o "$OUTFILE" main.go 17 | -------------------------------------------------------------------------------- /hack/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -o pipefail 5 | 6 | rm -rf .cover/ .test/ 7 | mkdir .cover/ .test/ 8 | trap "rm -rf .test/" EXIT 9 | for pkg in `go list ./... | grep -v /vendor/`; do 10 | go test -v -covermode=atomic \ 11 | -coverprofile=".cover/$(echo $pkg | sed 's/\//_/g').cover.out" $pkg 12 | done 13 | 14 | echo "mode: set" > .cover/cover.out && cat .cover/*.cover.out | grep -v mode: | sort -r | \ 15 | awk '{if($1 != last) {print $0;last=$1}}' >> .cover/cover.out 16 | 17 | go tool cover -html=.cover/cover.out -o=.cover/coverage.html 18 | 19 | CODECOV_BASH_URL=https://codecov.io/bash 20 | if [ "$CODECOV_TOKEN" != "" ]; then curl -s $CODECOV_BASH_URL | bash -s; fi -------------------------------------------------------------------------------- /iris/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | version: 0.1.0 3 | description: Watch on Kubernetes event, filter and send them as standard wehbook you any system 4 | name: iris 5 | keywords: 6 | - kubernetes 7 | - webhook 8 | - codefresh 9 | - slack 10 | icon: https://github.com/olegsu/iris/raw/master/Iris.jpg 11 | sources: 12 | - https://github.com/olegsu/iris 13 | maintainers: 14 | - name: Oleg Sucharevich 15 | email: oleg2807@gmail.com 16 | -------------------------------------------------------------------------------- /iris/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | */}} 13 | {{- define "fullname" -}} 14 | {{- $name := default .Chart.Name .Values.nameOverride -}} 15 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 16 | {{- end -}} -------------------------------------------------------------------------------- /iris/templates/cm.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | iris: | 4 | 5 | {{- if .Values.filters }} 6 | filters: 7 | {{ toYaml .Values.filters | indent 8 }} 8 | {{- end }} 9 | 10 | {{- if .Values.destinations }} 11 | destinations: 12 | {{ toYaml .Values.destinations | indent 8 }} 13 | {{- end }} 14 | 15 | {{- if .Values.integrations }} 16 | integrations: 17 | {{ toYaml .Values.integrations | indent 8 }} 18 | {{- end }} 19 | 20 | kind: ConfigMap 21 | metadata: 22 | name: {{ template "fullname" . }} 23 | namespace: {{ .Values.namespace }} -------------------------------------------------------------------------------- /iris/templates/deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: {{ template "fullname" . }} 5 | namespace: {{ .Values.namespace }} 6 | labels: 7 | app: {{ template "fullname" . }} 8 | chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} 9 | release: {{ .Release.Name | quote }} 10 | heritage: {{ .Release.Service | quote }} 11 | version: {{ .Values.image.tag | quote }} 12 | spec: 13 | replicas: 1 14 | template: 15 | metadata: 16 | labels: 17 | app: {{ template "fullname" . }} 18 | spec: 19 | containers: 20 | - env: 21 | - name: IRIS_FILE 22 | value: {{ .Values.configmap.mountPath }} 23 | name: {{ template "fullname" . }} 24 | command: 25 | - "iris" 26 | - "run" 27 | imagePullPolicy: {{ .Values.imagePullPolicy | quote }} 28 | image: {{ .Values.image.name }}:{{ .Values.image.tag }} 29 | volumes: 30 | - name: config 31 | configMap: 32 | name: {{ template "fullname" . }} 33 | mountPath: {{ .Values.configmap.mountPath }} -------------------------------------------------------------------------------- /iris/templates/namespace.yaml: -------------------------------------------------------------------------------- 1 | {{- if ne .Values.namespace "default" }} 2 | 3 | apiVersion: v1 4 | kind: Namespace 5 | metadata: 6 | name: {{ .Values.namespace }} 7 | 8 | {{- end }} -------------------------------------------------------------------------------- /iris/templates/role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: {{ template "fullname" . }} 5 | rules: 6 | - apiGroups: [""] 7 | verbs: ["get", "list", "watch"] 8 | resources: ["events", "pods"] 9 | 10 | --- 11 | apiVersion: rbac.authorization.k8s.io/v1 12 | kind: ClusterRoleBinding 13 | metadata: 14 | name: {{ template "fullname" . }} 15 | roleRef: 16 | apiGroup: rbac.authorization.k8s.io 17 | kind: ClusterRole 18 | name: {{ template "fullname" . }} 19 | subjects: 20 | - kind: ServiceAccount 21 | name: default 22 | namespace: {{ .Values.namespace }} 23 | -------------------------------------------------------------------------------- /iris/values.yaml: -------------------------------------------------------------------------------- 1 | image: 2 | name: olsynt/iris 3 | tag: latest 4 | 5 | namespace: iris 6 | 7 | imagePullPolicy: IfNotPresent 8 | 9 | configmap: 10 | mountPath: /iris/iris.yaml -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Copyright © 2019 oleg2807@gmail.com 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import "github.com/olegsu/iris/cmd" 18 | 19 | func main() { 20 | cmd.Execute() 21 | } 22 | -------------------------------------------------------------------------------- /pkg/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/olegsu/iris/pkg/dal" 5 | "github.com/olegsu/iris/pkg/kube" 6 | "github.com/olegsu/iris/pkg/logger" 7 | "github.com/olegsu/iris/pkg/reader" 8 | "github.com/olegsu/iris/pkg/server" 9 | ) 10 | 11 | type ApplicationOptions struct { 12 | IrisPath string 13 | KubeconfigPath string 14 | InCluster bool 15 | Logger logger.Logger 16 | } 17 | 18 | func NewApplicationOptions(irisconfig string, kubeconfig string, incluster bool, logger logger.Logger) *ApplicationOptions { 19 | return &ApplicationOptions{ 20 | IrisPath: irisconfig, 21 | KubeconfigPath: kubeconfig, 22 | InCluster: incluster, 23 | Logger: logger, 24 | } 25 | } 26 | 27 | func CreateApp(config *ApplicationOptions) { 28 | k := kube.NewKubeManager(config.KubeconfigPath, config.InCluster, config.Logger) 29 | var r reader.IRISProcessor 30 | 31 | r, _ = reader.NewProcessor([]string{ 32 | config.IrisPath, 33 | }, k) 34 | bytes, _ := reader.Process(r) 35 | d := dal.CreateDalFromBytes(bytes, k, config.Logger) 36 | fn := func(obj interface{}) { 37 | onAdd(d.Integrations, obj) 38 | } 39 | go k.Watch(fn) 40 | server.StartServer(config.Logger) 41 | } 42 | 43 | func onAdd(integrations []*dal.Integration, obj interface{}) { 44 | for _, integration := range integrations { 45 | go integration.Exec(obj) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pkg/dal/dal.go: -------------------------------------------------------------------------------- 1 | package dal 2 | 3 | import ( 4 | "github.com/olegsu/iris/pkg/destination" 5 | "github.com/olegsu/iris/pkg/filter" 6 | "github.com/olegsu/iris/pkg/kube" 7 | "github.com/olegsu/iris/pkg/logger" 8 | "github.com/olegsu/iris/pkg/util" 9 | ) 10 | 11 | var dal *Dal 12 | 13 | type Dal struct { 14 | DestinationService destination.Service 15 | Integrations []*Integration 16 | FilterService filter.Service 17 | Logger logger.Logger 18 | } 19 | 20 | func GetDal() *Dal { 21 | return dal 22 | } 23 | 24 | func CreateDalFromBytes(bytes []byte, k kube.Kube, logger logger.Logger) *Dal { 25 | d := &Dal{ 26 | Logger: logger, 27 | } 28 | var data map[string][]map[string]interface{} 29 | util.UnmarshalOrDie(bytes, &data, logger) 30 | 31 | d.FilterService = filter.NewService(filter.NewFactory(logger), data["filters"], k, logger) 32 | 33 | d.DestinationService = destination.NewService(data["destinations"], k, logger) 34 | 35 | for _, integration := range data["integrations"] { 36 | i := &Integration{} 37 | util.MapToObjectOrDie(integration, i, logger) 38 | i.logger = logger.New("Integration-Name", i.Name) 39 | d.Integrations = append(d.Integrations, i) 40 | } 41 | dal = d 42 | return d 43 | } 44 | -------------------------------------------------------------------------------- /pkg/dal/integration.go: -------------------------------------------------------------------------------- 1 | package dal 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/olegsu/iris/pkg/destination" 7 | "github.com/olegsu/iris/pkg/filter" 8 | "github.com/olegsu/iris/pkg/logger" 9 | 10 | v1 "k8s.io/api/core/v1" 11 | ) 12 | 13 | type Integration struct { 14 | Name string `yaml:"name"` 15 | Filters []string `yaml:"filters"` 16 | Destinations []string `yaml:"destinations"` 17 | logger logger.Logger 18 | } 19 | 20 | func (i *Integration) Exec(obj interface{}) (bool, error) { 21 | ev := obj.(*v1.Event) 22 | var j interface{} 23 | bytes, err := json.Marshal(&ev) 24 | if err != nil { 25 | return false, nil 26 | } 27 | json.Unmarshal(bytes, &j) 28 | result := true 29 | result = filter.IsFiltersMatched(GetDal().FilterService, i.Filters, j, i.logger) 30 | if result == true { 31 | i.logger.Debug("All checks are passed, executing", "name", i.Name) 32 | destination.Exec(GetDal().DestinationService, i.Destinations, obj, i.logger) 33 | } 34 | return false, nil 35 | } 36 | -------------------------------------------------------------------------------- /pkg/destination/codefresh_destination.go: -------------------------------------------------------------------------------- 1 | package destination 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | 11 | v1 "k8s.io/api/core/v1" 12 | ) 13 | 14 | type codefreshDestination struct { 15 | baseDestination `yaml:",inline"` 16 | Branch string `yaml:"branch"` 17 | Pipeline string `yaml:"pipeline"` 18 | CFToken string `yaml:"cftoken"` 19 | } 20 | 21 | type codefreshPostRequestBody struct { 22 | Options map[string]string `json:"options"` 23 | Variables map[string]string `json:"variables"` 24 | Contexts []string `json:"contexts"` 25 | Branch string `json:"branch"` 26 | } 27 | 28 | func (d *codefreshDestination) Exec(payload interface{}) { 29 | postBody := &codefreshPostRequestBody{ 30 | Variables: make(map[string]string), 31 | } 32 | if d.Branch != "" { 33 | postBody.Branch = d.Branch 34 | } 35 | var ev *v1.Event 36 | b, _ := json.Marshal(payload) 37 | json.Unmarshal(b, &ev) 38 | 39 | postBody.Variables["IRIS_RESOURCE_NAME"] = ev.InvolvedObject.Name 40 | postBody.Variables["IRIS_NAMESPACE"] = ev.InvolvedObject.Namespace 41 | 42 | mJSON, _ := json.Marshal(postBody) 43 | contentReader := bytes.NewReader(mJSON) 44 | 45 | url := fmt.Sprintf("https://g.codefresh.io/api/pipelines/run/%s", url.QueryEscape(d.Pipeline)) 46 | d.logger.Debug("Executing Codefresh destination\n") 47 | req, _ := http.NewRequest("POST", url, contentReader) 48 | req.Header.Set("authorization", d.CFToken) 49 | req.Header.Set("User-Agent", UserAgent) 50 | req.Header.Set("Content-Type", "application/json") 51 | client := &http.Client{} 52 | resp, _ := client.Do(req) 53 | defer resp.Body.Close() 54 | body, _ := ioutil.ReadAll(resp.Body) 55 | if resp.StatusCode == 200 { 56 | d.logger.Debug("Build started", "ID", string(body)) 57 | } else { 58 | d.logger.Debug("Error!", "Status_Code", resp.StatusCode, "message", string(body)) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /pkg/destination/default_destination.go: -------------------------------------------------------------------------------- 1 | package destination 2 | 3 | import ( 4 | "bytes" 5 | "crypto/hmac" 6 | "crypto/sha256" 7 | "encoding/base64" 8 | "encoding/json" 9 | "net/http" 10 | ) 11 | 12 | type defaultDestination struct { 13 | baseDestination `yaml:",inline"` 14 | URL string `yaml:"url"` 15 | Secret string `yaml:"secret"` 16 | Headers map[string]string `yaml:"headers"` 17 | } 18 | 19 | func getHmac(secret string, payload []byte) string { 20 | if secret != "" { 21 | key := []byte(secret) 22 | mac := hmac.New(sha256.New, key) 23 | mac.Write(payload) 24 | hmac := base64.URLEncoding.EncodeToString(mac.Sum(nil)) 25 | return hmac 26 | } 27 | return "" 28 | } 29 | 30 | func (d *defaultDestination) Exec(payload interface{}) { 31 | d.logger.Debug("Executing default destination", "name", d.Name, "URL", d.URL) 32 | mJSON, _ := json.Marshal(payload) 33 | contentReader := bytes.NewReader(mJSON) 34 | req, _ := http.NewRequest("POST", d.URL, contentReader) 35 | req.Header.Set("X-IRIS-HMAC", getHmac(d.Secret, mJSON)) 36 | req.Header.Set("Content-Type", "application/json") 37 | req.Header.Set("User-Agent", UserAgent) 38 | for k, v := range d.Headers { 39 | req.Header.Set(k, v) 40 | } 41 | client := &http.Client{} 42 | client.Do(req) 43 | } 44 | -------------------------------------------------------------------------------- /pkg/destination/destination.go: -------------------------------------------------------------------------------- 1 | package destination 2 | 3 | import ( 4 | "github.com/olegsu/iris/pkg/kube" 5 | "github.com/olegsu/iris/pkg/logger" 6 | "github.com/olegsu/iris/pkg/util" 7 | ) 8 | 9 | const ( 10 | TypeDefault = "" 11 | TypeCodefresh = "codefresh" 12 | ) 13 | 14 | var ( 15 | UserAgent = "iris/" + util.BuildVersion 16 | ) 17 | 18 | type Destination interface { 19 | GetName() string 20 | Exec(payload interface{}) 21 | GetType() string 22 | } 23 | 24 | func NewDestination(json map[string]interface{}, k kube.Kube, logger logger.Logger) Destination { 25 | var destination Destination 26 | if json["type"] != nil { 27 | destination = &codefreshDestination{ 28 | baseDestination: baseDestination{ 29 | logger: logger, 30 | }, 31 | } 32 | } else { 33 | destination = &defaultDestination{ 34 | baseDestination: baseDestination{ 35 | logger: logger, 36 | }, 37 | } 38 | } 39 | util.MapToObjectOrDie(json, destination, logger) 40 | return destination 41 | } 42 | 43 | type baseDestination struct { 44 | Name string `yaml:"name"` 45 | Type string `yaml:"type"` 46 | logger logger.Logger 47 | } 48 | 49 | func (d *baseDestination) GetName() string { 50 | return d.Name 51 | } 52 | 53 | func (d *baseDestination) GetType() string { 54 | return d.Type 55 | } 56 | -------------------------------------------------------------------------------- /pkg/destination/service.go: -------------------------------------------------------------------------------- 1 | package destination 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/olegsu/iris/pkg/kube" 7 | "github.com/olegsu/iris/pkg/logger" 8 | ) 9 | 10 | var d *dal 11 | 12 | type dal struct { 13 | destinations []Destination 14 | logger logger.Logger 15 | } 16 | 17 | // Service 18 | type Service interface { 19 | GetDestinationByName(string) (Destination, error) 20 | } 21 | 22 | // GetDestinationByName - finds a filters if exist 23 | func (d *dal) GetDestinationByName(name string) (Destination, error) { 24 | var destination Destination 25 | if d.destinations == nil { 26 | return nil, fmt.Errorf("No destination %s", name) 27 | } 28 | for index := 0; index < len(d.destinations); index++ { 29 | filterName := d.destinations[index].GetName() 30 | if filterName == name { 31 | destination = d.destinations[index] 32 | } 33 | } 34 | if destination == nil { 35 | return nil, fmt.Errorf("%s destination not found", name) 36 | } 37 | return destination, nil 38 | } 39 | 40 | // NewService - creates net Dal from json array of filters 41 | func NewService(destinationArray []map[string]interface{}, k kube.Kube, logger logger.Logger) Service { 42 | tempDal := &dal{ 43 | logger: logger, 44 | } 45 | for _, json := range destinationArray { 46 | f := NewDestination(json, k, logger) 47 | tempDal.destinations = append(tempDal.destinations, f) 48 | } 49 | d = tempDal 50 | return tempDal 51 | } 52 | 53 | func Exec(serivce Service, names []string, payload interface{}, logger logger.Logger) { 54 | for _, name := range names { 55 | dest, err := serivce.GetDestinationByName(name) 56 | if err != nil { 57 | logger.Error("Error", "err", err.Error()) 58 | } else { 59 | dest.Exec(payload) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pkg/filter/any_filter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | type anyFilter struct { 4 | baseFilter `yaml:",inline"` 5 | Filters []string `yaml:"filters"` 6 | Service Service 7 | } 8 | 9 | func (f *anyFilter) Apply(data interface{}) (bool, error) { 10 | result := false 11 | var err error 12 | for _, name := range f.Filters { 13 | filter, err := f.Service.GetFilterByName(name) 14 | if err != nil { 15 | return false, err 16 | } 17 | res, err := filter.Apply(data) 18 | if err != nil { 19 | return false, err 20 | } 21 | if res == true { 22 | result = true 23 | } 24 | } 25 | return result, err 26 | } 27 | -------------------------------------------------------------------------------- /pkg/filter/factory.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/olegsu/iris/pkg/kube" 8 | "github.com/olegsu/iris/pkg/logger" 9 | "github.com/olegsu/iris/pkg/util" 10 | ) 11 | 12 | // Factory interface builds filters 13 | type Factory interface { 14 | Build(map[string]interface{}, Service, kube.Kube) (Filter, error) 15 | } 16 | 17 | type f struct { 18 | logger logger.Logger 19 | } 20 | 21 | // Build build actual filter and return Filter interface 22 | func (_f *f) Build(json map[string]interface{}, s Service, k kube.Kube) (Filter, error) { 23 | if json["type"] != nil { 24 | t := strings.ToLower(json["type"].(string)) 25 | var f Filter 26 | switch t { 27 | case TypeReason: 28 | f = &reasonFilter{ 29 | baseFilter: baseFilter{ 30 | logger: _f.logger, 31 | }, 32 | } 33 | break 34 | case TypeNamespace: 35 | f = &namespaceFilter{ 36 | baseFilter: baseFilter{ 37 | logger: _f.logger, 38 | }, 39 | } 40 | break 41 | case TypeJSONPath: 42 | f = &jsonPathFilter{ 43 | baseFilter: baseFilter{ 44 | logger: _f.logger, 45 | }, 46 | } 47 | break 48 | case TypeLabel: 49 | f = &labelFilter{ 50 | kube: k, 51 | baseFilter: baseFilter{ 52 | logger: _f.logger, 53 | }, 54 | } 55 | break 56 | case TypeAny: 57 | f = &anyFilter{ 58 | Service: s, 59 | baseFilter: baseFilter{ 60 | logger: _f.logger, 61 | }, 62 | } 63 | break 64 | } 65 | if f == nil { 66 | return nil, fmt.Errorf("Type %s is not supported", json["type"]) 67 | } 68 | util.MapToObjectOrDie(json, f, _f.logger) 69 | return f, nil 70 | } else { 71 | return nil, fmt.Errorf("Type passed to filter %v", json) 72 | } 73 | } 74 | 75 | // NewFactory create new factory 76 | func NewFactory(logger logger.Logger) Factory { 77 | return &f{ 78 | logger: logger, 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /pkg/filter/factory_internal_test.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/olegsu/iris/pkg/logger" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestNewFactory(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | }{ 15 | { 16 | name: "Get filter factory", 17 | }, 18 | } 19 | for _, tt := range tests { 20 | t.Run(tt.name, func(t *testing.T) { 21 | message := fmt.Sprintf("Failed to get factory") 22 | factory := NewFactory(logger.New(nil)) 23 | assert.IsType(t, &f{}, factory, message) 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pkg/filter/factory_test.go: -------------------------------------------------------------------------------- 1 | package filter_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/olegsu/iris/pkg/filter" 8 | "github.com/olegsu/iris/pkg/filter/mocks" 9 | kube "github.com/olegsu/iris/pkg/kube/mocks" 10 | "github.com/olegsu/iris/pkg/logger" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func Test_f_Build(t *testing.T) { 15 | tests := []struct { 16 | name string 17 | buildJSONArg func() map[string]interface{} 18 | want filter.Filter 19 | wantErr bool 20 | }{ 21 | { 22 | name: "Build filte with reason type", 23 | wantErr: false, 24 | buildJSONArg: func() map[string]interface{} { 25 | return map[string]interface{}{ 26 | "type": "reason", 27 | "name": "filter-name", 28 | "reason": "reason", 29 | } 30 | }, 31 | }, 32 | { 33 | name: "Build filte with namespace type", 34 | wantErr: false, 35 | buildJSONArg: func() map[string]interface{} { 36 | return map[string]interface{}{ 37 | "type": "namespace", 38 | "name": "filter-name", 39 | "namespace": "default", 40 | } 41 | }, 42 | }, 43 | { 44 | name: "Build filte with jsonpath type with value match", 45 | wantErr: false, 46 | buildJSONArg: func() map[string]interface{} { 47 | return map[string]interface{}{ 48 | "type": "jsonpath", 49 | "name": "filter-name", 50 | "path": "$.path", 51 | "value": "value", 52 | } 53 | }, 54 | }, 55 | { 56 | name: "Build filte with jsonpath type with regexp match", 57 | wantErr: false, 58 | buildJSONArg: func() map[string]interface{} { 59 | return map[string]interface{}{ 60 | "type": "jsonpath", 61 | "name": "filter-name", 62 | "path": "$.path", 63 | "regexp": ".*", 64 | } 65 | }, 66 | }, 67 | { 68 | name: "Build filte with label type", 69 | wantErr: false, 70 | buildJSONArg: func() map[string]interface{} { 71 | return map[string]interface{}{ 72 | "type": "labels", 73 | "name": "filter-name", 74 | "labels": map[string]string{ 75 | "version": "v1", 76 | }, 77 | } 78 | }, 79 | }, 80 | { 81 | name: "Build filte with any type", 82 | wantErr: false, 83 | buildJSONArg: func() map[string]interface{} { 84 | return map[string]interface{}{ 85 | "type": "any", 86 | "name": "filter-name", 87 | "filters": []string{ 88 | "filter-1", 89 | "filter-2", 90 | }, 91 | } 92 | }, 93 | }, 94 | { 95 | name: "Return error when type not exit", 96 | wantErr: true, 97 | buildJSONArg: func() map[string]interface{} { 98 | return map[string]interface{}{} 99 | }, 100 | want: nil, 101 | }, 102 | { 103 | name: "Return error when type not supported", 104 | wantErr: true, 105 | buildJSONArg: func() map[string]interface{} { 106 | return map[string]interface{}{ 107 | "type": "not-supported", 108 | } 109 | }, 110 | }, 111 | } 112 | for _, tt := range tests { 113 | t.Run(tt.name, func(t *testing.T) { 114 | l := logger.New(nil) 115 | factory := filter.NewFactory(l) 116 | got, err := factory.Build(tt.buildJSONArg(), &mocks.Service{}, &kube.Kube{}) 117 | assert.Equalf(t, tt.wantErr, err != nil, "f.Build() error = %v, wantErr %v", err, tt.wantErr) 118 | 119 | if tt.wantErr == false { 120 | message := fmt.Sprintf("Returned object does not implement Filter interface") 121 | assert.Implements(t, (*filter.Filter)(nil), got, message) 122 | } 123 | }) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /pkg/filter/filter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "github.com/olegsu/iris/pkg/logger" 5 | ) 6 | 7 | const ( 8 | TypeReason = "reason" 9 | TypeNamespace = "namespace" 10 | TypeJSONPath = "jsonpath" 11 | TypeLabel = "labels" 12 | TypeAny = "any" 13 | ) 14 | 15 | type Filter interface { 16 | GetName() string 17 | Apply(interface{}) (bool, error) 18 | GetType() string 19 | } 20 | 21 | func ApplyFilter(f Filter, obj interface{}) (bool, error) { 22 | return f.Apply(obj) 23 | } 24 | 25 | type baseFilter struct { 26 | Name string `yaml:"name"` 27 | Type string `yaml:"type"` 28 | logger logger.Logger 29 | } 30 | 31 | func (f *baseFilter) GetName() string { 32 | return f.Name 33 | } 34 | 35 | func (f *baseFilter) GetType() string { 36 | return f.Type 37 | } 38 | -------------------------------------------------------------------------------- /pkg/filter/filter_apply_test.go: -------------------------------------------------------------------------------- 1 | package filter_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/mock" 9 | 10 | "github.com/olegsu/iris/pkg/filter" 11 | "github.com/olegsu/iris/pkg/logger" 12 | "github.com/stretchr/testify/assert" 13 | 14 | mocks "github.com/olegsu/iris/pkg/filter/mocks" 15 | 16 | kubeMock "github.com/olegsu/iris/pkg/kube/mocks" 17 | ) 18 | 19 | func Test_Filter_Apply(t *testing.T) { 20 | type args struct { 21 | data interface{} 22 | } 23 | tests := []struct { 24 | name string 25 | getFilter func(string) filter.Filter 26 | args args 27 | expectedResult bool 28 | expectedErr bool 29 | filterType string 30 | }{ 31 | { 32 | filterType: filter.TypeAny, 33 | name: "Should success when at least one inner filter is success", 34 | getFilter: func(t string) filter.Filter { 35 | // build the actual tested filter 36 | factory := filter.NewFactory(logger.New(nil)) 37 | 38 | mockedFactory := &mocks.Factory{} 39 | service := &mocks.Service{} 40 | mockedFilter1 := &mocks.Filter{} 41 | mockedFilter2 := &mocks.Filter{} 42 | 43 | anyFilter, _ := factory.Build(generateFilterJSON(t, "TEST", []string{"JSON-1", "JSON-2"}), service, nil) 44 | mockedFilter1.On("Apply", mock.Anything).Return(true, nil) 45 | mockedFilter2.On("Apply", mock.Anything).Return(false, nil) 46 | 47 | service.On("GetFilterByName", "JSON-1").Return(mockedFilter1, nil) 48 | service.On("GetFilterByName", "JSON-2").Return(mockedFilter2, nil) 49 | 50 | mockedFactory.On("Build", generateFilterJSON("jsonpath", "JSON-1", []string{"JSON-1"}), service, nil).Return(mockedFilter1, nil) 51 | mockedFactory.On("Build", generateFilterJSON("jsonpath", "JSON-2", []string{"JSON-2"}), service, nil).Return(mockedFilter2, nil) 52 | 53 | return anyFilter 54 | }, 55 | args: args{}, 56 | expectedResult: true, 57 | expectedErr: false, 58 | }, 59 | { 60 | filterType: filter.TypeAny, 61 | name: "Should fail when GetFilterByName returns an error", 62 | getFilter: func(t string) filter.Filter { 63 | // build the actual tested filter 64 | factory := filter.NewFactory(logger.New(nil)) 65 | 66 | service := &mocks.Service{} 67 | 68 | anyFilter, _ := factory.Build(generateFilterJSON(t, "TEST", []string{"JSON-1", "JSON-2"}), service, nil) 69 | 70 | service.On("GetFilterByName", "JSON-1").Return(nil, errors.New("Error!")) 71 | 72 | return anyFilter 73 | }, 74 | args: args{}, 75 | expectedResult: false, 76 | expectedErr: true, 77 | }, 78 | { 79 | filterType: filter.TypeAny, 80 | name: "Should fail when filter.Apply returns an error", 81 | getFilter: func(t string) filter.Filter { 82 | // build the actual tested filter 83 | factory := filter.NewFactory(logger.New(nil)) 84 | 85 | mockedFactory := &mocks.Factory{} 86 | service := &mocks.Service{} 87 | mockedFilter := &mocks.Filter{} 88 | 89 | anyFilter, _ := factory.Build(generateFilterJSON(t, "TEST", []string{"JSON-1", "JSON-2"}), service, nil) 90 | mockedFilter.On("Apply", mock.Anything).Return(false, errors.New("Error!")) 91 | 92 | service.On("GetFilterByName", "JSON-1").Return(mockedFilter, nil) 93 | 94 | mockedFactory.On("Build", generateFilterJSON("jsonpath", "JSON-1", []string{"JSON-1"}), service, nil).Return(mockedFilter, nil) 95 | 96 | return anyFilter 97 | }, 98 | args: args{}, 99 | expectedResult: false, 100 | expectedErr: true, 101 | }, 102 | { 103 | name: "Should success mathcing json by exact value", 104 | filterType: filter.TypeJSONPath, 105 | getFilter: func(t string) filter.Filter { 106 | // build the actual tested filter 107 | factory := filter.NewFactory(logger.New(nil)) 108 | 109 | service := &mocks.Service{} 110 | 111 | jsonpathFilter, _ := factory.Build(generateFilterJSON(t, "TEST", map[string]interface{}{ 112 | "value": "Value", 113 | }), service, nil) 114 | 115 | return jsonpathFilter 116 | }, 117 | args: args{ 118 | data: map[string]interface{}{ 119 | "root": "Value", 120 | }, 121 | }, 122 | expectedResult: true, 123 | expectedErr: false, 124 | }, 125 | { 126 | name: "Should failed mathcing json by exact value not match", 127 | filterType: filter.TypeJSONPath, 128 | getFilter: func(t string) filter.Filter { 129 | // build the actual tested filter 130 | factory := filter.NewFactory(logger.New(nil)) 131 | 132 | service := &mocks.Service{} 133 | 134 | jsonpathFilter, _ := factory.Build(generateFilterJSON(t, "TEST", map[string]interface{}{ 135 | "value": "value", 136 | }), service, nil) 137 | 138 | return jsonpathFilter 139 | }, 140 | args: args{ 141 | data: map[string]interface{}{ 142 | "root": "Value", 143 | }, 144 | }, 145 | expectedResult: false, 146 | expectedErr: false, 147 | }, 148 | { 149 | name: "Should fail mathcing json by exact value", 150 | filterType: filter.TypeJSONPath, 151 | getFilter: func(t string) filter.Filter { 152 | // build the actual tested filter 153 | factory := filter.NewFactory(logger.New(nil)) 154 | 155 | service := &mocks.Service{} 156 | 157 | jsonpathFilter, _ := factory.Build(generateFilterJSON(t, "TEST", map[string]interface{}{}), service, nil) 158 | 159 | return jsonpathFilter 160 | }, 161 | args: args{ 162 | data: map[string]interface{}{ 163 | "root": "not-mached", 164 | }, 165 | }, 166 | expectedResult: false, 167 | expectedErr: false, 168 | }, 169 | { 170 | name: "Should success mathcing any value using regexp", 171 | filterType: filter.TypeJSONPath, 172 | getFilter: func(t string) filter.Filter { 173 | // build the actual tested filter 174 | factory := filter.NewFactory(logger.New(nil)) 175 | 176 | service := &mocks.Service{} 177 | 178 | jsonpathFilter, _ := factory.Build(generateFilterJSON(t, "TEST", map[string]interface{}{ 179 | "regexp": ".", // match anything 180 | }), service, nil) 181 | 182 | return jsonpathFilter 183 | }, 184 | args: args{ 185 | data: map[string]interface{}{ 186 | "root": "Value", 187 | }, 188 | }, 189 | expectedResult: true, 190 | expectedErr: false, 191 | }, 192 | { 193 | name: "Should fail matching value using regexp", 194 | filterType: filter.TypeJSONPath, 195 | getFilter: func(t string) filter.Filter { 196 | // build the actual tested filter 197 | factory := filter.NewFactory(logger.New(nil)) 198 | 199 | service := &mocks.Service{} 200 | 201 | jsonpathFilter, _ := factory.Build(generateFilterJSON(t, "TEST", map[string]interface{}{ 202 | "regexp": "VALUE", // match anything 203 | }), service, nil) 204 | 205 | return jsonpathFilter 206 | }, 207 | args: args{ 208 | data: map[string]interface{}{ 209 | "root": "Value", 210 | }, 211 | }, 212 | expectedResult: false, 213 | expectedErr: false, 214 | }, 215 | { 216 | name: "Should fail when reading from json failed", 217 | filterType: filter.TypeJSONPath, 218 | getFilter: func(t string) filter.Filter { 219 | // build the actual tested filter 220 | factory := filter.NewFactory(logger.New(nil)) 221 | 222 | service := &mocks.Service{} 223 | 224 | jsonpathFilter, _ := factory.Build(generateFilterJSON(t, "TEST", map[string]interface{}{ 225 | "regexp": "VALUE", // match VALUE 226 | }), service, nil) 227 | 228 | return jsonpathFilter 229 | }, 230 | args: args{}, 231 | expectedResult: false, 232 | expectedErr: true, 233 | }, 234 | { 235 | name: "Should fail when regexp is not valid", 236 | filterType: filter.TypeJSONPath, 237 | getFilter: func(t string) filter.Filter { 238 | // build the actual tested filter 239 | factory := filter.NewFactory(logger.New(nil)) 240 | 241 | service := &mocks.Service{} 242 | 243 | jsonpathFilter, _ := factory.Build(generateFilterJSON(t, "TEST", map[string]interface{}{ 244 | "regexp": "(?:", // regex not valid 245 | }), service, nil) 246 | 247 | return jsonpathFilter 248 | }, 249 | args: args{ 250 | data: map[string]interface{}{ 251 | "root": "Value", 252 | }, 253 | }, 254 | expectedResult: false, 255 | expectedErr: true, 256 | }, 257 | { 258 | name: "Should fail when regexp or value not exist", 259 | filterType: filter.TypeJSONPath, 260 | getFilter: func(t string) filter.Filter { 261 | // build the actual tested filter 262 | factory := filter.NewFactory(logger.New(nil)) 263 | 264 | service := &mocks.Service{} 265 | 266 | jsonpathFilter, _ := factory.Build(generateFilterJSON(t, "TEST", map[string]interface{}{}), service, nil) 267 | 268 | return jsonpathFilter 269 | }, 270 | args: args{ 271 | data: map[string]interface{}{ 272 | "root": "Value", 273 | }, 274 | }, 275 | expectedResult: false, 276 | expectedErr: false, 277 | }, 278 | { 279 | name: "Should success when namespace matched", 280 | filterType: filter.TypeNamespace, 281 | getFilter: func(t string) filter.Filter { 282 | // build the actual tested filter 283 | factory := filter.NewFactory(logger.New(nil)) 284 | 285 | service := &mocks.Service{} 286 | 287 | namespaceFilter, _ := factory.Build(generateFilterJSON(t, "TEST", map[string]interface{}{ 288 | "namespace": "default", 289 | }), service, nil) 290 | 291 | return namespaceFilter 292 | }, 293 | args: args{ 294 | data: map[string]interface{}{ 295 | "metadata": map[string]interface{}{ 296 | "namespace": "default", 297 | }, 298 | }, 299 | }, 300 | expectedResult: true, 301 | expectedErr: false, 302 | }, 303 | { 304 | name: "Should fail when namespace not matched", 305 | filterType: filter.TypeNamespace, 306 | getFilter: func(t string) filter.Filter { 307 | // build the actual tested filter 308 | factory := filter.NewFactory(logger.New(nil)) 309 | 310 | service := &mocks.Service{} 311 | 312 | namespaceFilter, _ := factory.Build(generateFilterJSON(t, "TEST", map[string]interface{}{ 313 | "namespace": "default", 314 | }), service, nil) 315 | 316 | return namespaceFilter 317 | }, 318 | args: args{ 319 | data: map[string]interface{}{ 320 | "metadata": map[string]interface{}{ 321 | "namespace": "iris", 322 | }, 323 | }, 324 | }, 325 | expectedResult: false, 326 | expectedErr: false, 327 | }, 328 | { 329 | name: "Should success when reason matched", 330 | filterType: filter.TypeReason, 331 | getFilter: func(t string) filter.Filter { 332 | // build the actual tested filter 333 | factory := filter.NewFactory(logger.New(nil)) 334 | 335 | service := &mocks.Service{} 336 | 337 | reasonFilter, _ := factory.Build(generateFilterJSON(t, "TEST", map[string]interface{}{ 338 | "reason": "Scheduled", 339 | }), service, nil) 340 | 341 | return reasonFilter 342 | }, 343 | args: args{ 344 | data: map[string]interface{}{ 345 | "reason": "Scheduled", 346 | }, 347 | }, 348 | expectedResult: true, 349 | expectedErr: false, 350 | }, 351 | { 352 | name: "Should fail when reason not matched", 353 | filterType: filter.TypeReason, 354 | getFilter: func(t string) filter.Filter { 355 | // build the actual tested filter 356 | factory := filter.NewFactory(logger.New(nil)) 357 | 358 | service := &mocks.Service{} 359 | 360 | reasonFilter, _ := factory.Build(generateFilterJSON(t, "TEST", map[string]interface{}{ 361 | "reason": "Scheduled", 362 | }), service, nil) 363 | 364 | return reasonFilter 365 | }, 366 | args: args{ 367 | data: map[string]interface{}{ 368 | "reason": "Died", 369 | }, 370 | }, 371 | expectedResult: false, 372 | expectedErr: false, 373 | }, 374 | { 375 | name: "Should success when reason matched", 376 | filterType: filter.TypeLabel, 377 | getFilter: func(t string) filter.Filter { 378 | // build the actual tested filter 379 | factory := filter.NewFactory(logger.New(nil)) 380 | 381 | service := &mocks.Service{} 382 | kube := &kubeMock.Kube{} 383 | kube.On("ResourceByLabelsExist", mock.Anything, mock.Anything).Return(true, nil) 384 | reasonFilter, _ := factory.Build(generateFilterJSON(t, "TEST", map[string]interface{}{ 385 | "app-version": "v1", 386 | }), service, kube) 387 | 388 | return reasonFilter 389 | }, 390 | args: args{ 391 | data: map[string]interface{}{ 392 | "app-version": "v1", 393 | }, 394 | }, 395 | expectedResult: true, 396 | expectedErr: false, 397 | }, 398 | } 399 | for _, tt := range tests { 400 | name := fmt.Sprintf("Filter type: %s .%s", tt.filterType, tt.name) 401 | t.Run(name, func(t *testing.T) { 402 | fil := tt.getFilter(tt.filterType) 403 | got, err := fil.Apply(tt.args.data) 404 | assert.Equalf(t, tt.expectedErr, err != nil, "filter.Apply() error = %v, expectedErr %v", err, tt.expectedErr) 405 | 406 | if tt.expectedErr == false { 407 | assert.Equalf(t, tt.expectedResult, got, "filter.Apply() = %v, want %v", got, tt.expectedResult) 408 | } 409 | }) 410 | } 411 | } 412 | -------------------------------------------------------------------------------- /pkg/filter/filter_test.go: -------------------------------------------------------------------------------- 1 | package filter_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/olegsu/iris/pkg/filter" 7 | "github.com/olegsu/iris/pkg/filter/mocks" 8 | "github.com/stretchr/testify/mock" 9 | ) 10 | 11 | func TestApplyFilter(t *testing.T) { 12 | type args struct { 13 | f func() filter.Filter 14 | obj interface{} 15 | } 16 | tests := []struct { 17 | name string 18 | args args 19 | want bool 20 | wantErr bool 21 | }{ 22 | { 23 | name: "Should call apply on filter", 24 | args: args{ 25 | f: func() filter.Filter { 26 | m := &mocks.Filter{} 27 | m.On("Apply", mock.Anything).Return(true, nil) 28 | return m 29 | }, 30 | }, 31 | want: true, 32 | wantErr: false, 33 | }, 34 | } 35 | for _, tt := range tests { 36 | t.Run(tt.name, func(t *testing.T) { 37 | got, err := filter.ApplyFilter(tt.args.f(), tt.args.obj) 38 | if (err != nil) != tt.wantErr { 39 | t.Errorf("ApplyFilter() error = %v, wantErr %v", err, tt.wantErr) 40 | return 41 | } 42 | if got != tt.want { 43 | t.Errorf("ApplyFilter() = %v, want %v", got, tt.want) 44 | } 45 | }) 46 | } 47 | } 48 | 49 | func GenerateJSON() map[string]interface{} { 50 | return map[string]interface{}{ 51 | "name": "name", 52 | "type": "jsonpath", 53 | } 54 | } 55 | 56 | // func Test_baseFilter(t *testing.T) { 57 | 58 | // tests := []struct { 59 | // name string 60 | // getFilter func() filter.Filter 61 | // wantName string 62 | // wantType string 63 | // }{ 64 | // { 65 | // name: "Get filter name", 66 | // getFilter: func() filter.Filter { 67 | // factory := filter.NewFactory() 68 | // f, _ := factory.Build(GenerateJSON(), &kube.Kube{}) 69 | // return f 70 | // }, 71 | // wantName: "name", 72 | // wantType: "jsonpath", 73 | // }, 74 | // } 75 | // for _, tt := range tests { 76 | // t.Run(tt.name, func(t *testing.T) { 77 | // f := tt.getFilter() 78 | // if got := f.GetName(); got != tt.wantName { 79 | // t.Errorf("GetName() = %v, want %v", got, tt.wantName) 80 | // } 81 | 82 | // if got := f.GetType(); got != tt.wantType { 83 | // t.Errorf("GetName() = %v, want %v", got, tt.wantType) 84 | // } 85 | // }) 86 | // } 87 | // } 88 | 89 | func Test_baseFilter_GetType(t *testing.T) { 90 | 91 | tests := []struct { 92 | name string 93 | getFilter func() filter.Filter 94 | want string 95 | }{ 96 | { 97 | name: "Get filter name", 98 | getFilter: func() filter.Filter { 99 | f := &mocks.Filter{} 100 | f.On("GetType", mock.Anything).Return("filter-type") 101 | return f 102 | }, 103 | want: "filter-type", 104 | }, 105 | } 106 | for _, tt := range tests { 107 | t.Run(tt.name, func(t *testing.T) { 108 | f := tt.getFilter() 109 | if got := f.GetType(); got != tt.want { 110 | t.Errorf("GetType() = %v, want %v", got, tt.want) 111 | } 112 | }) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /pkg/filter/helpers_test.go: -------------------------------------------------------------------------------- 1 | package filter_test 2 | 3 | import ( 4 | "github.com/olegsu/iris/pkg/filter" 5 | ) 6 | 7 | func generateFilterJSON(t string, name string, data interface{}) map[string]interface{} { 8 | var r map[string]interface{} 9 | switch t { 10 | case filter.TypeAny: 11 | r = generateAnyFilter(name, data) 12 | break 13 | case filter.TypeJSONPath: 14 | r = generateJSONPathFilter(name, data) 15 | break 16 | case filter.TypeNamespace: 17 | r = generateNamespaceFilter(name, data) 18 | break 19 | case filter.TypeReason: 20 | r = generateReasonFilter(name, data) 21 | break 22 | case filter.TypeLabel: 23 | r = generateLabelFilter(name, data) 24 | break 25 | } 26 | return r 27 | } 28 | 29 | func generateAnyFilter(name string, data interface{}) map[string]interface{} { 30 | if name == "" { 31 | name = "generated" 32 | } 33 | return map[string]interface{}{ 34 | "name": name, 35 | "type": filter.TypeAny, 36 | "filters": data, 37 | } 38 | } 39 | 40 | func generateJSONPathFilter(name string, data interface{}) map[string]interface{} { 41 | var res map[string]interface{} 42 | var path string 43 | if name == "" { 44 | name = "generated" 45 | } 46 | 47 | res = map[string]interface{}{ 48 | "name": name, 49 | "type": filter.TypeJSONPath, 50 | } 51 | 52 | path = "$.root" 53 | 54 | casted, ok := data.(map[string]interface{}) 55 | 56 | if ok { 57 | if casted["regexp"] != nil { 58 | res["regexp"] = casted["regexp"] 59 | } else if casted["value"] != nil { 60 | res["value"] = casted["value"] 61 | } 62 | } 63 | 64 | res["path"] = path 65 | return res 66 | } 67 | 68 | func generateNamespaceFilter(name string, data interface{}) map[string]interface{} { 69 | var res map[string]interface{} 70 | 71 | res = map[string]interface{}{ 72 | "name": name, 73 | "type": filter.TypeNamespace, 74 | } 75 | 76 | casted, ok := data.(map[string]interface{}) 77 | if ok { 78 | res["namespace"] = casted["namespace"] 79 | } 80 | return res 81 | } 82 | 83 | func generateReasonFilter(name string, data interface{}) map[string]interface{} { 84 | var res map[string]interface{} 85 | 86 | res = map[string]interface{}{ 87 | "name": name, 88 | "type": filter.TypeReason, 89 | } 90 | 91 | casted, ok := data.(map[string]interface{}) 92 | if ok { 93 | res["reason"] = casted["reason"] 94 | } 95 | return res 96 | } 97 | 98 | func generateLabelFilter(name string, data interface{}) map[string]interface{} { 99 | var res map[string]interface{} 100 | 101 | res = map[string]interface{}{ 102 | "name": name, 103 | "type": filter.TypeLabel, 104 | "lables": data, 105 | } 106 | return res 107 | } 108 | -------------------------------------------------------------------------------- /pkg/filter/jsonpath_filter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/olegsu/iris/pkg/logger" 7 | "github.com/yalp/jsonpath" 8 | ) 9 | 10 | type jsonPathFilter struct { 11 | baseFilter `yaml:",inline"` 12 | Path string `yaml:"path"` 13 | Value string `yaml:"value"` 14 | Regexp string `yaml:"regexp"` 15 | } 16 | 17 | func (f *jsonPathFilter) Apply(data interface{}) (bool, error) { 18 | path := f.Path 19 | actualValue, err := jsonpath.Read(data, path) 20 | if err != nil { 21 | return false, err 22 | } 23 | if f.Value != "" { 24 | res := applyMatchValueFilter(f.Value, actualValue.(string), f.logger) 25 | return res, nil 26 | } else if f.Regexp != "" { 27 | res, err := applyRegexpFilter(f.Regexp, actualValue.(string), f.logger) 28 | if err != nil { 29 | return false, err 30 | } 31 | return res, nil 32 | } else { 33 | return false, nil 34 | } 35 | } 36 | 37 | func applyRegexpFilter(pattern string, value string, logger logger.Logger) (bool, error) { 38 | match, err := regexp.MatchString(pattern, value) 39 | if err != nil { 40 | return false, err 41 | } 42 | if match == false { 43 | logger.Debug("JSON path does not match to regex", "pattern", pattern, "value", value) 44 | return false, nil 45 | } 46 | logger.Debug("JSON path match to regex", "pattern", pattern, "value", value) 47 | return true, nil 48 | } 49 | 50 | func applyMatchValueFilter(requiredValue string, actualValue string, logger logger.Logger) bool { 51 | if actualValue != requiredValue { 52 | logger.Debug("JSON path does not match", "requiredValue", requiredValue, "actualValue", actualValue) 53 | return false 54 | } 55 | logger.Debug("JSON path match", "requiredValue", requiredValue, "actualValue", actualValue) 56 | return true 57 | } 58 | -------------------------------------------------------------------------------- /pkg/filter/label_filter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import "github.com/olegsu/iris/pkg/kube" 4 | 5 | type labelFilter struct { 6 | baseFilter `yaml:",inline"` 7 | Labels map[string]string `yaml:"labels"` 8 | kube kube.Kube 9 | } 10 | 11 | func (f *labelFilter) Apply(data interface{}) (bool, error) { 12 | return f.kube.ResourceByLabelsExist(data, f.Labels) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/filter/mocks/Factory.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import filter "github.com/olegsu/iris/pkg/filter" 6 | import kube "github.com/olegsu/iris/pkg/kube" 7 | import mock "github.com/stretchr/testify/mock" 8 | 9 | // Factory is an autogenerated mock type for the Factory type 10 | type Factory struct { 11 | mock.Mock 12 | } 13 | 14 | // Build provides a mock function with given fields: _a0, _a1, _a2 15 | func (_m *Factory) Build(_a0 map[string]interface{}, _a1 filter.Service, _a2 kube.Kube) (filter.Filter, error) { 16 | ret := _m.Called(_a0, _a1, _a2) 17 | 18 | var r0 filter.Filter 19 | if rf, ok := ret.Get(0).(func(map[string]interface{}, filter.Service, kube.Kube) filter.Filter); ok { 20 | r0 = rf(_a0, _a1, _a2) 21 | } else { 22 | if ret.Get(0) != nil { 23 | r0 = ret.Get(0).(filter.Filter) 24 | } 25 | } 26 | 27 | var r1 error 28 | if rf, ok := ret.Get(1).(func(map[string]interface{}, filter.Service, kube.Kube) error); ok { 29 | r1 = rf(_a0, _a1, _a2) 30 | } else { 31 | r1 = ret.Error(1) 32 | } 33 | 34 | return r0, r1 35 | } 36 | -------------------------------------------------------------------------------- /pkg/filter/mocks/Filter.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // Filter is an autogenerated mock type for the Filter type 8 | type Filter struct { 9 | mock.Mock 10 | } 11 | 12 | // Apply provides a mock function with given fields: _a0 13 | func (_m *Filter) Apply(_a0 interface{}) (bool, error) { 14 | ret := _m.Called(_a0) 15 | 16 | var r0 bool 17 | if rf, ok := ret.Get(0).(func(interface{}) bool); ok { 18 | r0 = rf(_a0) 19 | } else { 20 | r0 = ret.Get(0).(bool) 21 | } 22 | 23 | var r1 error 24 | if rf, ok := ret.Get(1).(func(interface{}) error); ok { 25 | r1 = rf(_a0) 26 | } else { 27 | r1 = ret.Error(1) 28 | } 29 | 30 | return r0, r1 31 | } 32 | 33 | // GetName provides a mock function with given fields: 34 | func (_m *Filter) GetName() string { 35 | ret := _m.Called() 36 | 37 | var r0 string 38 | if rf, ok := ret.Get(0).(func() string); ok { 39 | r0 = rf() 40 | } else { 41 | r0 = ret.Get(0).(string) 42 | } 43 | 44 | return r0 45 | } 46 | 47 | // GetType provides a mock function with given fields: 48 | func (_m *Filter) GetType() string { 49 | ret := _m.Called() 50 | 51 | var r0 string 52 | if rf, ok := ret.Get(0).(func() string); ok { 53 | r0 = rf() 54 | } else { 55 | r0 = ret.Get(0).(string) 56 | } 57 | 58 | return r0 59 | } 60 | -------------------------------------------------------------------------------- /pkg/filter/mocks/Service.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import filter "github.com/olegsu/iris/pkg/filter" 6 | import mock "github.com/stretchr/testify/mock" 7 | 8 | // Service is an autogenerated mock type for the Service type 9 | type Service struct { 10 | mock.Mock 11 | } 12 | 13 | // GetFilterByName provides a mock function with given fields: _a0 14 | func (_m *Service) GetFilterByName(_a0 string) (filter.Filter, error) { 15 | ret := _m.Called(_a0) 16 | 17 | var r0 filter.Filter 18 | if rf, ok := ret.Get(0).(func(string) filter.Filter); ok { 19 | r0 = rf(_a0) 20 | } else { 21 | if ret.Get(0) != nil { 22 | r0 = ret.Get(0).(filter.Filter) 23 | } 24 | } 25 | 26 | var r1 error 27 | if rf, ok := ret.Get(1).(func(string) error); ok { 28 | r1 = rf(_a0) 29 | } else { 30 | r1 = ret.Error(1) 31 | } 32 | 33 | return r0, r1 34 | } 35 | -------------------------------------------------------------------------------- /pkg/filter/namespace_filter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | type namespaceFilter struct { 4 | baseFilter `yaml:",inline"` 5 | Namespace string 6 | } 7 | 8 | func (f *namespaceFilter) Apply(data interface{}) (bool, error) { 9 | jsonFilter := &jsonPathFilter{ 10 | baseFilter: baseFilter{ 11 | Name: f.GetName(), 12 | Type: f.GetType(), 13 | logger: f.logger, 14 | }, 15 | Path: "$.metadata.namespace", 16 | Value: f.Namespace, 17 | } 18 | return jsonFilter.Apply(data) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/filter/reason_filter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | type reasonFilter struct { 4 | baseFilter `yaml:",inline"` 5 | Reason string `yaml:"reason"` 6 | } 7 | 8 | func (f *reasonFilter) Apply(data interface{}) (bool, error) { 9 | jsonFilter := &jsonPathFilter{ 10 | baseFilter: baseFilter{ 11 | Name: f.GetName(), 12 | Type: f.GetType(), 13 | logger: f.logger, 14 | }, 15 | Path: "$.reason", 16 | Value: f.Reason, 17 | } 18 | return jsonFilter.Apply(data) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/filter/service.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/olegsu/iris/pkg/kube" 7 | "github.com/olegsu/iris/pkg/logger" 8 | ) 9 | 10 | type dal struct { 11 | filters []Filter 12 | logger logger.Logger 13 | } 14 | 15 | // Service is the service of the filter package 16 | type Service interface { 17 | GetFilterByName(string) (Filter, error) 18 | } 19 | 20 | // GetFilterByName - finds a filters if exist 21 | func (d *dal) GetFilterByName(name string) (Filter, error) { 22 | var f Filter 23 | for index := 0; index < len(d.filters); index++ { 24 | filterName := d.filters[index].GetName() 25 | if filterName == name { 26 | f = d.filters[index] 27 | } 28 | } 29 | if f == nil { 30 | return nil, fmt.Errorf("%s filter not found", name) 31 | } 32 | return f, nil 33 | } 34 | 35 | // NewService - creates net Dal from json array of filters 36 | func NewService(factory Factory, filterArray []map[string]interface{}, k kube.Kube, logger logger.Logger) Service { 37 | tempDal := &dal{ 38 | filters: []Filter{}, 39 | logger: logger, 40 | } 41 | for _, json := range filterArray { 42 | f, _ := factory.Build(json, tempDal, k) 43 | tempDal.filters = append(tempDal.filters, f) 44 | } 45 | return tempDal 46 | } 47 | 48 | // IsFiltersMatched Go over all filters and apply each one on on data 49 | // Return true is the data matched to all the filters 50 | func IsFiltersMatched(service Service, requiredFilters []string, data interface{}, logger logger.Logger) bool { 51 | matched := true 52 | for _, f := range requiredFilters { 53 | var res bool 54 | filter, err := service.GetFilterByName(f) 55 | if err != nil { 56 | logger.Error("Error", "err", err.Error()) 57 | matched = false 58 | } else { 59 | res, err = filter.Apply(data) 60 | if err != nil { 61 | logger.Error("Error", "err", err.Error()) 62 | matched = false 63 | } 64 | if res == false { 65 | matched = false 66 | } 67 | } 68 | } 69 | 70 | return matched 71 | } 72 | -------------------------------------------------------------------------------- /pkg/filter/service_test.go: -------------------------------------------------------------------------------- 1 | package filter_test 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/olegsu/iris/pkg/filter" 9 | filterMock "github.com/olegsu/iris/pkg/filter/mocks" 10 | "github.com/olegsu/iris/pkg/kube" 11 | kubeMock "github.com/olegsu/iris/pkg/kube/mocks" 12 | "github.com/olegsu/iris/pkg/logger" 13 | "github.com/stretchr/testify/mock" 14 | ) 15 | 16 | func generateFilterAsJSONArray(len int) []map[string]interface{} { 17 | res := []map[string]interface{}{} 18 | for i := 0; i < len; i++ { 19 | toAdd := map[string]interface{}{ 20 | "type": "jsonpath", 21 | } 22 | res = append(res, toAdd) 23 | } 24 | return res 25 | } 26 | 27 | func TestNewService(t *testing.T) { 28 | type args struct { 29 | factory *filterMock.Factory 30 | filterArray []map[string]interface{} 31 | kube kube.Kube 32 | service filter.Service 33 | } 34 | 35 | tests := []struct { 36 | name string 37 | args args 38 | callCount int 39 | }{ 40 | { 41 | name: "Create service with no filters", 42 | args: args{ 43 | factory: &filterMock.Factory{}, 44 | kube: &kubeMock.Kube{}, 45 | filterArray: generateFilterAsJSONArray(0), 46 | }, 47 | callCount: 0, 48 | }, 49 | { 50 | name: "Create service one filter", 51 | args: args{ 52 | factory: &filterMock.Factory{}, 53 | kube: &kubeMock.Kube{}, 54 | filterArray: generateFilterAsJSONArray(1), 55 | }, 56 | callCount: 1, 57 | }, 58 | } 59 | for _, tt := range tests { 60 | t.Run(tt.name, func(t *testing.T) { 61 | l := logger.New(nil) 62 | if len(tt.args.filterArray) > 0 { 63 | tt.args.factory.On("Build", tt.args.filterArray[0], mock.Anything, tt.args.kube).Return(nil, nil) 64 | } 65 | filter.NewService(tt.args.factory, tt.args.filterArray, tt.args.kube, l) 66 | tt.args.factory.AssertNumberOfCalls(t, "Build", tt.callCount) 67 | }) 68 | } 69 | } 70 | 71 | func TestIsFiltersMatched(t *testing.T) { 72 | type args struct { 73 | getServiceFn func() filter.Service 74 | requiredFilters []string 75 | data interface{} 76 | } 77 | tests := []struct { 78 | name string 79 | args args 80 | want bool 81 | }{ 82 | { 83 | name: "Return true when no filters passed", 84 | args: args{ 85 | getServiceFn: func() filter.Service { 86 | return nil 87 | }, 88 | requiredFilters: []string{}, 89 | data: nil, 90 | }, 91 | want: true, 92 | }, 93 | { 94 | name: "Return true when all filters return truly value", 95 | args: args{ 96 | getServiceFn: func() filter.Service { 97 | s := &filterMock.Service{} 98 | f := &filterMock.Filter{} 99 | f. 100 | On("Apply", mock.Anything). 101 | Return(true, nil) 102 | s. 103 | On("GetFilterByName", mock.Anything). 104 | Return(f, nil) 105 | return s 106 | }, 107 | requiredFilters: []string{"filter-name"}, 108 | data: nil, 109 | }, 110 | want: true, 111 | }, 112 | { 113 | name: "Return false when at least one filter return falsy value", 114 | args: args{ 115 | getServiceFn: func() filter.Service { 116 | s := &filterMock.Service{} 117 | f := &filterMock.Filter{} 118 | f. 119 | On("Apply", mock.Anything). 120 | Return(false, nil) 121 | 122 | s. 123 | On("GetFilterByName", mock.Anything). 124 | Return(f, nil) 125 | return s 126 | }, 127 | requiredFilters: []string{ 128 | "filter-success", 129 | "filter-failed", 130 | }, 131 | data: nil, 132 | }, 133 | want: false, 134 | }, 135 | { 136 | name: "Return false when filter returns an error", 137 | args: args{ 138 | getServiceFn: func() filter.Service { 139 | s := &filterMock.Service{} 140 | f := &filterMock.Filter{} 141 | f. 142 | On("Apply", mock.Anything). 143 | Return(false, errors.New("Test!")) 144 | 145 | s. 146 | On("GetFilterByName", mock.Anything). 147 | Return(f, nil) 148 | return s 149 | }, 150 | requiredFilters: []string{ 151 | "filter-success", 152 | "filter-failed", 153 | }, 154 | data: nil, 155 | }, 156 | want: false, 157 | }, 158 | { 159 | name: "Return false getFilterByName returns an error", 160 | args: args{ 161 | getServiceFn: func() filter.Service { 162 | s := &filterMock.Service{} 163 | 164 | s. 165 | On("GetFilterByName", mock.Anything). 166 | Return(nil, errors.New("Test!")) 167 | return s 168 | }, 169 | requiredFilters: []string{ 170 | "filter-failed", 171 | }, 172 | data: nil, 173 | }, 174 | want: false, 175 | }, 176 | } 177 | for _, tt := range tests { 178 | t.Run(tt.name, func(t *testing.T) { 179 | l := logger.New(nil) 180 | got := filter.IsFiltersMatched(tt.args.getServiceFn(), tt.args.requiredFilters, tt.args.data, l) 181 | if got != tt.want { 182 | t.Errorf("IsFiltersMatched() = %v, want %v", got, tt.want) 183 | } 184 | }) 185 | } 186 | } 187 | 188 | func Test_dal_GetFilterByName(t *testing.T) { 189 | type fields struct { 190 | filters []filter.Filter 191 | } 192 | type args struct { 193 | name string 194 | } 195 | tests := []struct { 196 | name string 197 | fields fields 198 | args args 199 | want func(string) filter.Filter 200 | wantErr bool 201 | getFilterJSONArray func() []map[string]interface{} 202 | getFactory func(filter.Filter) filter.Factory 203 | }{ 204 | { 205 | name: "Should find filter when exist", 206 | args: args{ 207 | name: "filter-found", 208 | }, 209 | want: func(name string) filter.Filter { 210 | f := &filterMock.Filter{} 211 | f.On("GetName", mock.Anything).Return(name) 212 | return f 213 | }, 214 | getFilterJSONArray: func() []map[string]interface{} { 215 | return generateFilterAsJSONArray(1) 216 | }, 217 | getFactory: func(ret filter.Filter) filter.Factory { 218 | factoryMock := &filterMock.Factory{} 219 | factoryMock.On("Build", mock.Anything, mock.Anything, mock.Anything).Return(ret, nil) 220 | return factoryMock 221 | }, 222 | }, 223 | { 224 | name: "Should return error when filters are nil", 225 | want: func(name string) filter.Filter { 226 | return nil 227 | }, 228 | getFilterJSONArray: func() []map[string]interface{} { 229 | return nil 230 | }, 231 | getFactory: func(ret filter.Filter) filter.Factory { 232 | factoryMock := &filterMock.Factory{} 233 | factoryMock.On("Build", mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("Error")) 234 | return factoryMock 235 | }, 236 | wantErr: true, 237 | }, 238 | } 239 | for _, tt := range tests { 240 | t.Run(tt.name, func(t *testing.T) { 241 | l := logger.New(nil) 242 | want := tt.want(tt.args.name) 243 | factoryMock := tt.getFactory(want) 244 | filterArray := tt.getFilterJSONArray() 245 | d := filter.NewService(factoryMock, filterArray, &kubeMock.Kube{}, l) 246 | got, err := d.GetFilterByName(tt.args.name) 247 | if (err != nil) != tt.wantErr { 248 | t.Errorf("dal.GetFilterByName() error = %v, wantErr %v", err, tt.wantErr) 249 | return 250 | } 251 | 252 | if !reflect.DeepEqual(got, want) { 253 | t.Errorf("dal.GetFilterByName() = %v, want %v", got, want) 254 | } 255 | }) 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /pkg/kube/kube.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/olegsu/iris/pkg/logger" 9 | v1 "k8s.io/api/core/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/fields" 12 | "k8s.io/client-go/kubernetes" 13 | "k8s.io/client-go/rest" 14 | "k8s.io/client-go/tools/cache" 15 | "k8s.io/client-go/tools/clientcmd" 16 | ) 17 | 18 | type WatchFn func(obj interface{}) 19 | 20 | type Kube interface { 21 | Watch(WatchFn) 22 | ResourceByLabelsExist(interface{}, map[string]string) (bool, error) 23 | } 24 | 25 | type kube struct { 26 | Clientset *kubernetes.Clientset 27 | logger logger.Logger 28 | } 29 | 30 | func (k *kube) Watch(watchFn WatchFn) { 31 | k.logger.Debug("Starting kuberntes watcher") 32 | watchlist := cache.NewListWatchFromClient(k.Clientset.Core().RESTClient(), "events", metav1.NamespaceAll, fields.Everything()) 33 | _, controller := cache.NewInformer( 34 | watchlist, 35 | &v1.Event{}, 36 | time.Second*0, 37 | cache.ResourceEventHandlerFuncs{ 38 | AddFunc: func(obj interface{}) { 39 | watchFn(obj) 40 | }, 41 | }, 42 | ) 43 | stop := make(chan struct{}) 44 | go controller.Run(stop) 45 | for { 46 | time.Sleep(time.Second) 47 | } 48 | } 49 | 50 | func (k *kube) ResourceByLabelsExist(obj interface{}, labels map[string]string) (bool, error) { 51 | selector := "" 52 | for k, v := range labels { 53 | if selector == "" { 54 | selector = fmt.Sprintf("%s=%s", k, v) 55 | } else { 56 | selector = fmt.Sprintf("%s,%s=%s", selector, k, v) 57 | } 58 | } 59 | var ev *v1.Event 60 | bytes, err := json.Marshal(obj) 61 | json.Unmarshal(bytes, &ev) 62 | opt := metav1.ListOptions{ 63 | LabelSelector: selector, 64 | } 65 | pods, err := k.Clientset.CoreV1().Pods(ev.InvolvedObject.Namespace).List(opt) 66 | if err != nil { 67 | return false, err 68 | } 69 | return len(pods.Items) > 0, nil 70 | } 71 | 72 | func NewKubeManager(kubeconfig string, incluster bool, logger logger.Logger) Kube { 73 | k := &kube{ 74 | logger: logger, 75 | } 76 | var config *rest.Config 77 | var err error 78 | if incluster == true { 79 | logger.Debug("Running from in cluster") 80 | config, err = rest.InClusterConfig() 81 | } else { 82 | logger.Debug("Connecting to cluster from kubeconfig", "path", kubeconfig) 83 | config, err = clientcmd.BuildConfigFromFlags("", kubeconfig) 84 | } 85 | if err != nil { 86 | panic(err.Error()) 87 | } 88 | 89 | cs, err := kubernetes.NewForConfig(config) 90 | if err != nil { 91 | panic(err.Error()) 92 | } 93 | k.Clientset = cs 94 | return k 95 | } 96 | -------------------------------------------------------------------------------- /pkg/kube/mocks/Kube.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import kube "github.com/olegsu/iris/pkg/kube" 6 | import mock "github.com/stretchr/testify/mock" 7 | 8 | // Kube is an autogenerated mock type for the Kube type 9 | type Kube struct { 10 | mock.Mock 11 | } 12 | 13 | // ResourceByLabelsExist provides a mock function with given fields: _a0, _a1 14 | func (_m *Kube) ResourceByLabelsExist(_a0 interface{}, _a1 map[string]string) (bool, error) { 15 | ret := _m.Called(_a0, _a1) 16 | 17 | var r0 bool 18 | if rf, ok := ret.Get(0).(func(interface{}, map[string]string) bool); ok { 19 | r0 = rf(_a0, _a1) 20 | } else { 21 | r0 = ret.Get(0).(bool) 22 | } 23 | 24 | var r1 error 25 | if rf, ok := ret.Get(1).(func(interface{}, map[string]string) error); ok { 26 | r1 = rf(_a0, _a1) 27 | } else { 28 | r1 = ret.Error(1) 29 | } 30 | 31 | return r0, r1 32 | } 33 | 34 | // GetIRISConfigmap provides a mock function with given fields: _a0, _a1 35 | func (_m *Kube) GetIRISConfigmap(_a0 string, _a1 string) ([]byte, error) { 36 | ret := _m.Called(_a0, _a1) 37 | 38 | var r0 []byte 39 | if rf, ok := ret.Get(0).(func(string, string) []byte); ok { 40 | r0 = rf(_a0, _a1) 41 | } else { 42 | if ret.Get(0) != nil { 43 | r0 = ret.Get(0).([]byte) 44 | } 45 | } 46 | 47 | var r1 error 48 | if rf, ok := ret.Get(1).(func(string, string) error); ok { 49 | r1 = rf(_a0, _a1) 50 | } else { 51 | r1 = ret.Error(1) 52 | } 53 | 54 | return r0, r1 55 | } 56 | 57 | // Watch provides a mock function with given fields: _a0 58 | func (_m *Kube) Watch(_a0 kube.WatchFn) { 59 | _m.Called(_a0) 60 | } 61 | -------------------------------------------------------------------------------- /pkg/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | log "github.com/inconshreveable/log15" 5 | ) 6 | 7 | type ( 8 | Logger interface { 9 | log.Logger 10 | } 11 | 12 | Options struct { 13 | Command string 14 | Verbose bool 15 | } 16 | ) 17 | 18 | func New(opt *Options) Logger { 19 | var l Logger 20 | lvl := log.LvlError 21 | if opt != nil { 22 | l = log.New(log.Ctx{ 23 | "Command": opt.Command, 24 | }) 25 | if opt.Verbose { 26 | lvl = log.LvlDebug 27 | } 28 | } else { 29 | l = log.New() 30 | } 31 | handlers := []log.Handler{} 32 | verboseHandler := log.LvlFilterHandler(lvl, log.StdoutHandler) 33 | handlers = append(handlers, verboseHandler) 34 | l.SetHandler(log.MultiHandler(handlers...)) 35 | return l 36 | } 37 | -------------------------------------------------------------------------------- /pkg/reader/reader.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/olegsu/iris/pkg/kube" 7 | "github.com/olegsu/iris/pkg/util/reader/file" 8 | ) 9 | 10 | type ( 11 | // IRISProcessor knows how to process iris configs 12 | IRISProcessor interface { 13 | Process() ([]byte, error) 14 | } 15 | 16 | processor struct { 17 | fileReader file.FileReader 18 | args []string 19 | } 20 | ) 21 | 22 | // Process - start processing the iris confi 23 | func (i *processor) Process() ([]byte, error) { 24 | if i.fileReader != nil { 25 | return i.fileReader.Read(i.args[0]) 26 | } else { 27 | return nil, fmt.Errorf("No reader found") 28 | } 29 | } 30 | 31 | // NewProcessor - crete new processor based on the len of the args 32 | func NewProcessor(args []string, k kube.Kube) (IRISProcessor, error) { 33 | if len(args) == 1 { 34 | return &processor{ 35 | fileReader: file.NewFileReader(k), 36 | args: args, 37 | }, nil 38 | } else { 39 | return nil, fmt.Errorf("Could not create iris processor, arguments not match") 40 | } 41 | } 42 | 43 | // Process - execte processor 44 | func Process(processor IRISProcessor) ([]byte, error) { 45 | return processor.Process() 46 | } 47 | -------------------------------------------------------------------------------- /pkg/reader/reader_test.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/olegsu/iris/pkg/kube" 8 | 9 | "github.com/olegsu/iris/pkg/util/reader/file" 10 | ) 11 | 12 | type mockFileReader struct{} 13 | 14 | func (m *mockFileReader) Read(path string) ([]byte, error) { 15 | return []byte{}, nil 16 | } 17 | 18 | type mockKube struct{} 19 | 20 | func (m *mockKube) Watch(fn kube.WatchFn) { 21 | 22 | } 23 | 24 | func (m *mockKube) ResourceByLabelsExist(obj interface{}, labels map[string]string) (bool, error) { 25 | return true, nil 26 | } 27 | 28 | func Test_processor_Process(t *testing.T) { 29 | 30 | type fields struct { 31 | fileReader file.FileReader 32 | args []string 33 | } 34 | tests := []struct { 35 | name string 36 | fields fields 37 | want []byte 38 | wantErr bool 39 | }{ 40 | { 41 | name: "Should process from file", 42 | fields: fields{ 43 | fileReader: &mockFileReader{}, 44 | args: []string{ 45 | "path/to/file", 46 | }, 47 | }, 48 | want: []byte{}, 49 | wantErr: false, 50 | }, 51 | { 52 | name: "Should return error when no readers found ", 53 | fields: fields{ 54 | args: []string{}, 55 | }, 56 | want: nil, 57 | wantErr: true, 58 | }, 59 | } 60 | for _, tt := range tests { 61 | t.Run(tt.name, func(t *testing.T) { 62 | i := &processor{ 63 | fileReader: tt.fields.fileReader, 64 | args: tt.fields.args, 65 | } 66 | got, err := i.Process() 67 | if (err != nil) != tt.wantErr { 68 | t.Errorf("processor.Process() error = %v, wantErr %v", err, tt.wantErr) 69 | return 70 | } 71 | if !reflect.DeepEqual(got, tt.want) { 72 | t.Errorf("processor.Process() = %v, want %v", got, tt.want) 73 | } 74 | }) 75 | } 76 | } 77 | 78 | func TestNewProcessor(t *testing.T) { 79 | type args struct { 80 | args []string 81 | obj kube.Kube 82 | } 83 | tests := []struct { 84 | name string 85 | args args 86 | want IRISProcessor 87 | wantErr bool 88 | }{ 89 | { 90 | name: "Should create processor with file reader when given input len=1", 91 | args: args{ 92 | args: []string{"path/to/file"}, 93 | obj: &mockKube{}, 94 | }, 95 | want: &processor{ 96 | args: []string{"path/to/file"}, 97 | fileReader: file.NewFileReader(&mockKube{}), 98 | }, 99 | wantErr: false, 100 | }, 101 | { 102 | name: "Should return an error when args len is not 1 or 2", 103 | args: args{ 104 | args: []string{}, 105 | obj: &mockKube{}, 106 | }, 107 | want: nil, 108 | wantErr: true, 109 | }, 110 | } 111 | for _, tt := range tests { 112 | t.Run(tt.name, func(t *testing.T) { 113 | got, err := NewProcessor(tt.args.args, tt.args.obj) 114 | if (err != nil) != tt.wantErr { 115 | t.Errorf("NewProcessor() error = %v, wantErr %v", err, tt.wantErr) 116 | return 117 | } 118 | if !reflect.DeepEqual(got, tt.want) { 119 | t.Errorf("NewProcessor() = %v, want %v", got, tt.want) 120 | } 121 | }) 122 | } 123 | } 124 | 125 | type mockIRISProcessor struct{} 126 | 127 | func (m *mockIRISProcessor) Process() ([]byte, error) { 128 | return []byte{}, nil 129 | } 130 | 131 | func TestProcess(t *testing.T) { 132 | type args struct { 133 | processor IRISProcessor 134 | } 135 | tests := []struct { 136 | name string 137 | args args 138 | want []byte 139 | wantErr bool 140 | }{ 141 | { 142 | name: "Should call process on given processor", 143 | args: args{ 144 | processor: &mockIRISProcessor{}, 145 | }, 146 | want: []byte{}, 147 | wantErr: false, 148 | }, 149 | } 150 | for _, tt := range tests { 151 | t.Run(tt.name, func(t *testing.T) { 152 | got, err := Process(tt.args.processor) 153 | if (err != nil) != tt.wantErr { 154 | t.Errorf("Process() error = %v, wantErr %v", err, tt.wantErr) 155 | return 156 | } 157 | if !reflect.DeepEqual(got, tt.want) { 158 | t.Errorf("Process() = %v, want %v", got, tt.want) 159 | } 160 | }) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /pkg/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/olegsu/iris/pkg/logger" 6 | ) 7 | 8 | func StartServer(logger logger.Logger) { 9 | logger.Debug("Server started") 10 | r := gin.Default() 11 | r.Run() 12 | } 13 | -------------------------------------------------------------------------------- /pkg/util/reader/file/file.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "io/ioutil" 5 | 6 | "github.com/olegsu/iris/pkg/kube" 7 | ) 8 | 9 | // FileReader 10 | type FileReader interface { 11 | Read(string) ([]byte, error) 12 | } 13 | 14 | type reader struct { 15 | kube kube.Kube 16 | } 17 | 18 | // Read - reads the actual file from path 19 | func (r *reader) Read(path string) ([]byte, error) { 20 | return ioutil.ReadFile(path) 21 | } 22 | 23 | // NewFileReader - creates file reader 24 | func NewFileReader(k kube.Kube) FileReader { 25 | return &reader{ 26 | kube: k, 27 | } 28 | } 29 | 30 | // ProcessFile - execute filereader with path as input 31 | func ProcessFile(r FileReader, path string) ([]byte, error) { 32 | return r.Read(path) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/util/reader/file/file_test.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | 8 | kubeMock "github.com/olegsu/iris/pkg/kube/mocks" 9 | ) 10 | 11 | type mockFileReaderSuccess struct { 12 | FileReader 13 | } 14 | 15 | func (m *mockFileReaderSuccess) Read(string) ([]byte, error) { 16 | return []byte{}, nil 17 | } 18 | 19 | type mockFileReaderNotFound struct { 20 | FileReader 21 | } 22 | 23 | func (m *mockFileReaderNotFound) Read(path string) ([]byte, error) { 24 | return nil, fmt.Errorf("%s not found", path) 25 | } 26 | 27 | func TestProcessFile(t *testing.T) { 28 | type args struct { 29 | r FileReader 30 | path string 31 | } 32 | tests := []struct { 33 | name string 34 | args args 35 | want []byte 36 | wantErr bool 37 | }{ 38 | { 39 | name: "Should return file content", 40 | args: args{ 41 | r: &mockFileReaderSuccess{}, 42 | path: "fath/to/file", 43 | }, 44 | want: []byte{}, 45 | wantErr: false, 46 | }, 47 | { 48 | name: "Should return error if file not found", 49 | args: args{ 50 | r: &mockFileReaderNotFound{}, 51 | path: "file/not/found", 52 | }, 53 | want: nil, 54 | wantErr: true, 55 | }, 56 | } 57 | for _, tt := range tests { 58 | t.Run(tt.name, func(t *testing.T) { 59 | got, err := ProcessFile(tt.args.r, tt.args.path) 60 | if (err != nil) != tt.wantErr { 61 | t.Errorf("ProcessFile() error = %v, wantErr %v", err, tt.wantErr) 62 | return 63 | } 64 | if !reflect.DeepEqual(got, tt.want) { 65 | t.Errorf("ProcessFile() = %v, want %v", got, tt.want) 66 | } 67 | }) 68 | } 69 | } 70 | 71 | func TestNewFileReader(t *testing.T) { 72 | tests := []struct { 73 | name string 74 | want FileReader 75 | }{ 76 | { 77 | name: "Should return Filereader interface", 78 | want: &reader{ 79 | kube: &kubeMock.Kube{}, 80 | }, 81 | }, 82 | } 83 | 84 | for _, tt := range tests { 85 | t.Run(tt.name, func(t *testing.T) { 86 | if got := NewFileReader(&kubeMock.Kube{}); !reflect.DeepEqual(got, tt.want) { 87 | t.Errorf("NewFileReader() = %v, want %v", got, tt.want) 88 | } 89 | }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /pkg/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/olegsu/iris/pkg/logger" 8 | 9 | yaml "gopkg.in/yaml.v2" 10 | ) 11 | 12 | var ( 13 | BuildVersion, BuildDate, BuildCommit, BuildBy string 14 | ) 15 | 16 | func UnmarshalOrDie(in []byte, out interface{}, logger logger.Logger) { 17 | err := yaml.Unmarshal(in, out) 18 | if err != nil { 19 | logger.Error("Failed to unmarshal", "err", err.Error()) 20 | os.Exit(1) 21 | } 22 | } 23 | 24 | func MapToObjectOrDie(m map[string]interface{}, o interface{}, logger logger.Logger) { 25 | b, err := yaml.Marshal(m) 26 | if err != nil { 27 | logger.Error("Failed to marshal", "err", err.Error()) 28 | os.Exit(1) 29 | } 30 | err = yaml.Unmarshal(b, o) 31 | if err != nil { 32 | logger.Error("Failed to unmarshal", "err", err.Error()) 33 | os.Exit(1) 34 | } 35 | } 36 | 37 | func EchoError(err error) { 38 | fmt.Printf("Error: %s\n", err.Error()) 39 | } 40 | --------------------------------------------------------------------------------