├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── LICENSE ├── README.md ├── bin └── kestoeso ├── cmd ├── apply.go ├── generate.go └── root.go ├── go.mod ├── go.sum ├── main.go ├── migrate.sh ├── pkg ├── apis │ └── apis.go ├── apply │ ├── apply.go │ └── apply_test.go ├── parser │ ├── parser.go │ ├── parser_test.go │ └── testdata │ │ ├── aws-secretsmanager.golden │ │ ├── es_aws-secretsmanager.golden │ │ └── ss_aws-secretsmanager.golden ├── provider │ ├── provider.go │ └── provider_test.go └── utils │ └── utils.go └── rollback.sh /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - 'main' 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | - uses: actions/setup-go@v3 17 | with: 18 | go-version-file: 'go.mod' 19 | 20 | - name: test 21 | run: go test -v ./... 22 | 23 | - name: build 24 | run: go build -o kes2eso main.go 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | jobs: 9 | release: 10 | if: startsWith(github.ref, 'refs/tags/') 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Create Release 14 | uses: softprops/action-gh-release@v1 15 | with: 16 | generate_release_notes: true 17 | 18 | releases-matrix: 19 | name: Release Go Binary 20 | runs-on: ubuntu-latest 21 | permissions: 22 | packages: write 23 | contents: write 24 | id-token: write 25 | strategy: 26 | matrix: 27 | goos: [linux, windows, darwin] 28 | goarch: [amd64, arm64] 29 | exclude: 30 | - goarch: "386" 31 | goos: darwin 32 | - goarch: arm64 33 | goos: windows 34 | steps: 35 | - name: Checkout 36 | uses: actions/checkout@v3 37 | 38 | - name: Release binary 39 | uses: wangyoucao577/go-release-action@v1.31 40 | with: 41 | github_token: ${{ secrets.GITHUB_TOKEN }} 42 | release_tag: ${{ github.ref_name }} 43 | goos: ${{ matrix.goos }} 44 | goarch: ${{ matrix.goarch }} 45 | goversion: "https://dl.google.com/go/go1.19.1.linux-amd64.tar.gz" 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KES to ESO 2 | kes-to-eso is a tool driven to facilitate migrating from [kubernetes-external-secrets](https://github.com/external-secrets/kubernetes-external-secrets) to [external-secrets](https://github.com/external-secrets/external-secrets) 3 | 4 | It has a binary tool which makes the translation between `kes-ExternalSecrets` and `eso-ExternalSecrets+SecretStores`. By default, it creates `ClusterSecretStores` bound to any credentials already available for `kes`. Whenever a credential is stored only in environment variables, it will also output the appropriate secret to be created. 5 | 6 | # Usage 7 | 8 | The migration process can be done in two ways: manually, or automatically. 9 | 10 | ## Automatic Migration 11 | 12 | Automatic Migration is useful for any user that don't have any templated kes-files. 13 | 14 | ### Build Binary 15 | The binary in bin folder might not work on all architectures. The `bin/kestoeso` was observed to be not working on Mac M1 Pro. 16 | You can build the binary easily using the command `go build main.go`. The binary named main can be used instead of the `bin/kestoeso`. 17 | 18 | ``` 19 | vi migrate.sh # EDIT KES NAMESPACE AND ESO NAMESPACE ENV VARS 20 | ./migrate.sh 21 | ``` 22 | 23 | This script will run the following steps: 24 | * Download KES ExternalSecrets files from cluster and save them in `kes_files` folder 25 | * Scale ESO replicaset to 0 26 | * Run `kestoeso generate` to generate ESO ExternalSecrets+SecretStores in `eso_files` folder 27 | * Apply ESO ExternalSecrets+SecretStores in cluster 28 | * Scale KES replicaset to 0 29 | * run `kestoeso apply` on all namespaces to remove kes ownership from all kes-managed Secrets 30 | * Scale ESO replicaset to 1 31 | 32 | Rollback steps can be achieved by simply scaling KES replicaset to 1 and ESO replicaset to 0. This is also available at 33 | 34 | ``` 35 | ./rollback.sh 36 | ``` 37 | 38 | ## Manual Migration 39 | 40 | If you are unsure about the migration script, want to migrate only a given subset of ExternalSecrets or have custom templated kes files in your setup, a manual migration is recommended for you. In order to do so, here are the steps needed. 41 | 42 | 1) Have available / download KES external-secrets that you want to migrate. You can achieve that by running `bash -c "$(kubectl get externalsecrets.kubernetes-client.io -n -o=jsonpath='{range .items[*]}{"kubectl get externalsecrets.kubernetes-client.io -o yaml -n "}{.metadata.namespace}{" "}{.metadata.name}{" >> path/to/input/"}{.metadata.namespace}{"-"}{.metadata.name}{".yaml; "}{end}')"` for a full namespace download. 43 | 2) Generate ESO files by typing `kestoeso generate -i path/to/input -o path/to/output -n ` 44 | 3) Review generated files. `kestoeso` will output any warnings whenever a given kes input could not be properly translated. It will already template the file for you, so all you need to do is open that file and properly edit it. 45 | 4) Include any templated files: `kestoeso` will abort whenever it finds a `template` usage or a `path` usage in kes ExternalSecrets, skipping that file completly. 46 | 5) Create and update any ServiceAccount / Secret references that you think it might be needed. Update ClusterSecretStores to SecretStores, if desired 47 | 6) Apply generated ESO files to your deployment 48 | 7) Because ownership is still set to KES, and any KES ExternalSecret deletion would cause secret deletion, it is recommended to update the secret ownership to ESO. In order to do so, KES deployment must be off, otherwise it will steal ownership from ESO. After scaling KES to 0, you can manually edit each secret ownership, or use `kestoeso apply`. It is possible to select a given namespace and a given secret arrays to be changed, or a combination of both. `kestoeso apply` will manually remove any ownership from `kes` to let that secret be available to both `kes` and `eso`. IF eso is already available, secret ownership will be passed to `eso`. This can be checked with `kubectl get secrets -o yaml | grep -i ownerReferences -A10` 49 | 50 | 51 | ## Warnings 52 | * This migration process still uses secrets and service accounts created by and used by `kes`. Do not delete them before being sure that any provider authorization is already updated with a new serviceAccount for `eso` 53 | * If `kestoeso` outputs any warnings, do not apply externalSecrets to kubernetes! Although the apply will work correctly, that does not indicate a healthy behavior of the migration process! 54 | ## Limitations 55 | * Not possible to migrate templated ExternalSecrets definitions 56 | * Not possible to migrate ExternalSecrets that uses `path` in both `Data` or `DataFrom` definitions 57 | * Not posible to automatically generate appropriate `SecretStores` (although you can ask `kestoeso` to do so, you still need to create every secret and serviceAccount on the appropriate namespace where the `SecretStore` is created, besides reviewing any permissions on every provider). 58 | -------------------------------------------------------------------------------- /bin/kestoeso: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kes-to-eso/1754645b7b01599b98a7ad3098bfdfe05b31ac05/bin/kestoeso -------------------------------------------------------------------------------- /cmd/apply.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "kestoeso/pkg/apply" 6 | "os" 7 | "time" 8 | 9 | log "github.com/sirupsen/logrus" 10 | "github.com/spf13/cobra" 11 | "k8s.io/client-go/kubernetes" 12 | "k8s.io/client-go/tools/clientcmd" 13 | ) 14 | 15 | var applyCmd = &cobra.Command{ 16 | Use: "apply", 17 | Short: "kestoeso apply --all-secrets --all-namespaces", 18 | Long: `kestoeso apply allows users to quickly remove secret ownership from KES. 19 | This allows ESO to fetch ownership and enables a clean migration (i.e. no secrets being deleted). 20 | A valid kubeconfig is needed for the command to work. Currently, only default setup works. 21 | Examples: 22 | kestoeso apply --all-secrets --all-namespaces 23 | kestoeso apply -s mysecret,mysecret2 --namespace mynamespace 24 | kestoeso apply --all-secrets --target-owner another-kubernetes-client.io/v1`, 25 | Run: func(cmd *cobra.Command, args []string) { 26 | opt := apply.NewApplyOptions() 27 | opt.AllNamespaces, _ = cmd.Flags().GetBool("all-namespaces") 28 | opt.AllSecrets, _ = cmd.Flags().GetBool("all-secrets") 29 | opt.Namespace, _ = cmd.Flags().GetString("namespace") 30 | opt.TargetOwner, _ = cmd.Flags().GetString("target-owner") 31 | targetSecrets, _ := cmd.Flags().GetStringSlice("secrets") 32 | config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | clientset, err := kubernetes.NewForConfig(config) 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | client := apply.ApplyClient{ 41 | Client: clientset, 42 | Options: opt, 43 | } 44 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 45 | defer cancel() 46 | err = apply.Root(ctx, &client, targetSecrets) 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | os.Exit(0) 51 | 52 | }, 53 | } 54 | 55 | func init() { 56 | var empty = make([]string, 0) 57 | applyCmd.Flags().BoolP("all-namespaces", "A", false, "Updates secrets for All Namespaces") 58 | applyCmd.Flags().Bool("all-secrets", false, "updates all secrets from one namespace") 59 | applyCmd.Flags().StringP("namespace", "n", "default", "Target namespace to look up for secrets") 60 | applyCmd.Flags().StringSliceP("secrets", "s", empty, "list of secret names to be updated") 61 | applyCmd.Flags().String("target-owner", "kubernetes-client.io/v1", "Target ownership value that secrets are going to be updated") 62 | } 63 | -------------------------------------------------------------------------------- /cmd/generate.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "kestoeso/pkg/apis" 7 | "kestoeso/pkg/parser" 8 | "kestoeso/pkg/provider" 9 | "os" 10 | "time" 11 | 12 | log "github.com/sirupsen/logrus" 13 | 14 | "github.com/spf13/cobra" 15 | "k8s.io/client-go/kubernetes" 16 | "k8s.io/client-go/tools/clientcmd" 17 | ) 18 | 19 | var generateCmd = &cobra.Command{ 20 | Use: "generate", 21 | Short: "A tool to convert KES YAML files into ESO YAML files", 22 | Long: `kes-to-eso generate is a tool to allow quick conversion between 23 | kubernetes-external-secrets and external-secrets-operator. 24 | It reads kubernetes-external-secrets deployment declaration and uses 25 | this information alongside with any KES externalSecrets declaration to 26 | provide ESO SecretStores and ExternalSecrets definitions. 27 | Examples: 28 | kes-to-eso generate -i path/to/kes/files -o eso/output/dir --to-stdout=false 29 | kes-to-eso generate -i path/to/a/single.yaml --kes-namespace=my_custom_namespace 30 | kes-to-eso generate -i path/to/kes/files | kubectl apply -f -`, 31 | Run: func(cmd *cobra.Command, args []string) { 32 | log.SetOutput(os.Stderr) 33 | opt := apis.NewOptions() 34 | opt.ContainerName, _ = cmd.Flags().GetString("kes-container-name") 35 | opt.DeploymentName, _ = cmd.Flags().GetString("kes-deployment-name") 36 | opt.Namespace, _ = cmd.Flags().GetString("kes-namespace") 37 | opt.SecretStore, _ = cmd.Flags().GetBool("secret-store") 38 | opt.ToStdout, _ = cmd.Flags().GetBool("to-stdout") 39 | opt.InputPath, _ = cmd.Flags().GetString("input") 40 | opt.TargetNamespace, _ = cmd.Flags().GetString("target-namespace") 41 | opt.CopySecretRefs, _ = cmd.Flags().GetBool("copy-secret-refs") // TODO - IMPLEMENT THIS 42 | _, err := os.Stat(opt.InputPath) 43 | if err != nil { 44 | fmt.Println("Missing input path!") 45 | err := cmd.Help() 46 | if err != nil { 47 | os.Exit(1) 48 | } 49 | os.Exit(1) 50 | } 51 | opt.OutputPath, _ = cmd.Flags().GetString("output") 52 | fileinfo, err := os.Stat(opt.OutputPath) 53 | if !opt.ToStdout { 54 | if err != nil { 55 | fmt.Println("Output Path is not a path (to-stdout = false)") 56 | err := cmd.Help() 57 | if err != nil { 58 | os.Exit(1) 59 | } 60 | os.Exit(1) 61 | } else if fileinfo == nil { 62 | fmt.Println("Could not find path for output (to-stdout = false)") 63 | err := cmd.Help() 64 | if err != nil { 65 | os.Exit(1) 66 | } 67 | os.Exit(1) 68 | } else if !fileinfo.IsDir() { 69 | fmt.Println("output path is not a directory (to-stdout = false)") 70 | err := cmd.Help() 71 | if err != nil { 72 | os.Exit(1) 73 | } 74 | os.Exit(1) 75 | } 76 | 77 | } 78 | if opt.SecretStore && !opt.CopySecretRefs { 79 | log.Warnf("Warning! Backend Secret References are not being copied to the secret store namespaces! This could lead to unintended behavior (--secret-store=true --copy-secret-refs=false)") 80 | } 81 | config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) 82 | if err != nil { 83 | log.Fatal(err) 84 | } 85 | clientset, err := kubernetes.NewForConfig(config) 86 | if err != nil { 87 | log.Fatal(err) 88 | } 89 | client := provider.KesToEsoClient{ 90 | Client: clientset, 91 | Options: opt, 92 | } 93 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 94 | defer cancel() 95 | parser.Root(ctx, &client) 96 | os.Exit(0) 97 | 98 | }, 99 | } 100 | 101 | func init() { 102 | generateCmd.Flags().Bool("to-stdout", false, "print generated yamls to STDOUT") 103 | generateCmd.Flags().StringP("input", "i", "", "path to lookup for KES yamls") 104 | generateCmd.Flags().StringP("output", "o", "", "path ot save ESO-generated yamls") 105 | generateCmd.Flags().String("kes-deployment-name", "kubernetes-external-secrets", "name of KES deployment object") 106 | generateCmd.Flags().String("kes-container-name", "kubernetes-external-secrets", "name of KES container object") 107 | generateCmd.Flags().StringP("kes-namespace", "n", "default", "namespace where KES is installed") 108 | generateCmd.Flags().String("target-namespace", "", "namespace to install files (not recommended - overrides KES-ExternalSecrets definitions)") 109 | } 110 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var ( 11 | kubeconfig string 12 | rootCmd = &cobra.Command{ 13 | Use: "kestoeso", 14 | Short: "A tool to convert KES YAML files into ESO YAML files", 15 | Long: `kes-to-eso is a tool to allow quick conversion between 16 | kubernetes-external-secrets and external-secrets-operator. 17 | It reads kubernetes-external-secrets deployment declaration and uses 18 | this information alongside with any KES externalSecrets declaration to 19 | provide ESO SecretStores and ExternalSecrets definitions. 20 | Examples: 21 | kes-to-eso generate -i path/to/kes/files | kubectl apply -f - 22 | kes-to-eso apply --target-namespace=my-ns`, 23 | } 24 | ) 25 | 26 | func Execute() { 27 | cobra.CheckErr(rootCmd.Execute()) 28 | } 29 | 30 | func init() { 31 | rootCmd.AddCommand(generateCmd) 32 | rootCmd.AddCommand(applyCmd) 33 | rootCmd.PersistentFlags().StringVar(&kubeconfig, "kubeconfig", "", 34 | "kubeconfig path, defaults to $KUBECONFIG or $HOME/.kube/config") 35 | 36 | cobra.OnInitialize(initConfig) 37 | } 38 | 39 | func initConfig() { 40 | kenv := os.Getenv("KUBECONFIG") 41 | if kubeconfig == "" { 42 | if kenv != "" { 43 | kubeconfig = kenv 44 | } else { 45 | home, _ := os.UserHomeDir() 46 | kubeconfig = filepath.Join(home, ".kube", "config") 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module kestoeso 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/external-secrets/external-secrets v0.3.6 7 | github.com/sirupsen/logrus v1.7.0 8 | github.com/spf13/cobra v1.2.1 9 | github.com/spf13/viper v1.8.1 10 | github.com/stretchr/testify v1.7.0 11 | k8s.io/api v0.22.2 12 | k8s.io/apimachinery v0.22.2 13 | k8s.io/client-go v0.22.2 14 | sigs.k8s.io/yaml v1.3.0 15 | ) 16 | 17 | require ( 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/evanphx/json-patch v4.11.0+incompatible // indirect 20 | github.com/fsnotify/fsnotify v1.5.1 // indirect 21 | github.com/go-logr/logr v0.4.0 // indirect 22 | github.com/gogo/protobuf v1.3.2 // indirect 23 | github.com/golang/protobuf v1.5.2 // indirect 24 | github.com/google/go-cmp v0.5.6 // indirect 25 | github.com/google/gofuzz v1.2.0 // indirect 26 | github.com/googleapis/gnostic v0.5.5 // indirect 27 | github.com/hashicorp/hcl v1.0.1-vault // indirect 28 | github.com/imdario/mergo v0.3.12 // indirect 29 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 30 | github.com/json-iterator/go v1.1.11 // indirect 31 | github.com/magiconair/properties v1.8.5 // indirect 32 | github.com/mitchellh/mapstructure v1.4.1 // indirect 33 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 34 | github.com/modern-go/reflect2 v1.0.1 // indirect 35 | github.com/pelletier/go-toml v1.9.3 // indirect 36 | github.com/pkg/errors v0.9.1 // indirect 37 | github.com/pmezard/go-difflib v1.0.0 // indirect 38 | github.com/spf13/afero v1.6.0 // indirect 39 | github.com/spf13/cast v1.3.1 // indirect 40 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 41 | github.com/spf13/pflag v1.0.5 // indirect 42 | github.com/subosito/gotenv v1.2.0 // indirect 43 | golang.org/x/net v0.0.0-20210520170846-37e1c6afe023 // indirect 44 | golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect 45 | golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf // indirect 46 | golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect 47 | golang.org/x/text v0.3.6 // indirect 48 | golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect 49 | google.golang.org/appengine v1.6.7 // indirect 50 | google.golang.org/protobuf v1.27.1 // indirect 51 | gopkg.in/inf.v0 v0.9.1 // indirect 52 | gopkg.in/ini.v1 v1.62.0 // indirect 53 | gopkg.in/yaml.v2 v2.4.0 // indirect 54 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 55 | k8s.io/klog/v2 v2.9.0 // indirect 56 | k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e // indirect 57 | k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a // indirect 58 | sigs.k8s.io/controller-runtime v0.9.3 // indirect 59 | sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect 60 | ) 61 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "kestoeso/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /migrate.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -e 2 | KES_NAMESPACE="kes" 3 | ESO_NAMESPACE="es" 4 | # Have KES and ESO both installed 5 | #Step 0 manual step to get .yaml files for KES External Secrets 6 | ## can be done with: 7 | mkdir -p kes_files 8 | mkdir -p eso_files 9 | bash -c "$(kubectl get externalsecrets.kubernetes-client.io -A -o=jsonpath='{range .items[*]}{"kubectl get externalsecrets.kubernetes-client.io -o yaml -n "}{.metadata.namespace}{" "}{.metadata.name}{" >> kes_files/"}{.metadata.namespace}{"-"}{.metadata.name}{".yaml; "}{end}')" 10 | 11 | #Step 1 Scale ESO to 0 (safeguard, really) 12 | kubectl scale deployment -n $ESO_NAMESPACE external-secrets --replicas=0 13 | 14 | #Step 2 Generate ESO files and apply them 15 | bin/kestoeso generate -i kes_files -o eso_files -n $KES_NAMESPACE 16 | 17 | kubectl apply -f eso_files 18 | 19 | # Step 3 - Scale KES to 0 20 | kubectl scale deployment -n $KES_NAMESPACE kubernetes-external-secrets --replicas=0 21 | 22 | # Step 4 - Update Ownership references 23 | bin/kestoeso apply --all-secrets --all-namespaces # 24 | # kestoeso apply -n changeme-my-target-ns -s my-secret-1,my-secret-2 # Alternative for people that want to do a step-by-step migration 25 | 26 | # Step 5 - Scale ESO to 1 27 | kubectl scale deployment -n $ESO_NAMESPACE external-secrets --replicas=1 28 | -------------------------------------------------------------------------------- /pkg/apis/apis.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | type KESExternalSecretData struct { 8 | Key string 9 | Name string 10 | SecretType string `json:"secretType"` 11 | Property string 12 | Recursive string 13 | Path string 14 | VersionStage string 15 | Version string 16 | IsBinary bool `json:"isBinary"` 17 | } 18 | type KESExternalSecretSpec struct { 19 | BackendType string 20 | VaultMountPoint string 21 | VaultRole string 22 | KvVersion int 23 | KeyVaultName string 24 | ProjectID string 25 | RoleArn string 26 | Region string 27 | DataFrom []string 28 | Data []KESExternalSecretData 29 | Template map[string]interface{} 30 | } 31 | type KESExternalSecret struct { 32 | Kind string `json:"kind,omitempty"` 33 | ApiVersion string `json:"apiVersion,omitempty"` 34 | ObjectMeta metav1.ObjectMeta `json:"metadata"` 35 | Spec KESExternalSecretSpec 36 | } 37 | 38 | type KesToEsoOptions struct { 39 | Namespace string 40 | DeploymentName string 41 | ContainerName string 42 | InputPath string 43 | OutputPath string 44 | ToStdout bool 45 | SecretStore bool 46 | TargetNamespace string 47 | CopySecretRefs bool 48 | } 49 | 50 | func NewOptions() *KesToEsoOptions { 51 | t := KesToEsoOptions{ 52 | Namespace: "default", 53 | DeploymentName: "kubernetes-external-secrets", 54 | ContainerName: "kubernetes-external-secrets", 55 | InputPath: "", 56 | OutputPath: "", 57 | ToStdout: false, 58 | SecretStore: false, 59 | TargetNamespace: "", 60 | CopySecretRefs: false, 61 | } 62 | return &t 63 | } 64 | -------------------------------------------------------------------------------- /pkg/apply/apply.go: -------------------------------------------------------------------------------- 1 | package apply 2 | 3 | import ( 4 | "context" 5 | 6 | log "github.com/sirupsen/logrus" 7 | corev1 "k8s.io/api/core/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/client-go/kubernetes" 10 | ) 11 | 12 | type ApplyOptions struct { 13 | Namespace string 14 | AllNamespaces bool 15 | AllSecrets bool 16 | Name string 17 | TargetOwner string 18 | } 19 | 20 | func NewApplyOptions() *ApplyOptions { 21 | a := ApplyOptions{ 22 | Namespace: "default", 23 | AllNamespaces: false, 24 | AllSecrets: false, 25 | Name: "", 26 | TargetOwner: "kubernetes-external-secrets", 27 | } 28 | return &a 29 | } 30 | 31 | type ApplyClient struct { 32 | Options *ApplyOptions 33 | Client kubernetes.Interface 34 | } 35 | 36 | func mapSecrets(secrets []string) map[string]string { 37 | ans := map[string]string{} 38 | for _, secret := range secrets { 39 | ans[secret] = secret 40 | } 41 | return ans 42 | } 43 | 44 | func (c ApplyClient) updateSingleSecret(ctx context.Context, namespace string, secret *corev1.Secret) (bool, error) { 45 | for idx, owner := range secret.OwnerReferences { 46 | if owner.APIVersion == c.Options.TargetOwner && owner.Kind == "ExternalSecret" { 47 | log.Debugf("Secret %v/%v matches owner %v", secret.Namespace, secret.Name, c.Options.TargetOwner) 48 | tmpSecret := secret.DeepCopy() 49 | if len(tmpSecret.OwnerReferences) > 1 { 50 | tmpSecret.OwnerReferences[idx] = tmpSecret.OwnerReferences[len(tmpSecret.OwnerReferences)-1] 51 | tmpSecret.OwnerReferences = tmpSecret.OwnerReferences[:len(tmpSecret.OwnerReferences)-2] 52 | 53 | } else { 54 | tmpSecret.OwnerReferences = []metav1.OwnerReference{} 55 | } 56 | _, err := c.Client.CoreV1().Secrets(namespace).Update(ctx, tmpSecret, metav1.UpdateOptions{}) 57 | if err != nil { 58 | return false, err 59 | } 60 | log.Infof("Secret %v/%v updated successfully", secret.Namespace, secret.Name) 61 | return true, nil 62 | } 63 | } 64 | return false, nil 65 | } 66 | 67 | func (c ApplyClient) UpdateSecretsFromAll(ctx context.Context, secrets []string) (int, error) { 68 | secretMap := mapSecrets(secrets) 69 | secretList, err := c.Client.CoreV1().Secrets("").List(ctx, metav1.ListOptions{}) 70 | if err != nil { 71 | return 0, err 72 | } 73 | count := 0 74 | for _, secret := range secretList.Items { 75 | _, ok := secretMap[secret.Name] 76 | if ok { 77 | log.Debugf("Reading secret %v/%v", secret.Namespace, secret.Name) 78 | update, err := c.updateSingleSecret(ctx, secret.Namespace, &secret) 79 | if err != nil { 80 | return count, err 81 | } 82 | if update { 83 | count = count + 1 84 | } 85 | } 86 | } 87 | return count, nil 88 | } 89 | 90 | func (c ApplyClient) UpdateSecretsFromNamespace(ctx context.Context, secrets []string) (int, error) { 91 | secretMap := mapSecrets(secrets) 92 | secretList, err := c.Client.CoreV1().Secrets(c.Options.Namespace).List(ctx, metav1.ListOptions{}) 93 | if err != nil { 94 | return 0, err 95 | } 96 | count := 0 97 | for _, secret := range secretList.Items { 98 | _, ok := secretMap[secret.Name] 99 | if ok { 100 | log.Debugf("Reading secret %v/%v", secret.Namespace, secret.Name) 101 | update, err := c.updateSingleSecret(ctx, c.Options.Namespace, &secret) 102 | if err != nil { 103 | return count, err 104 | } 105 | if update { 106 | count = count + 1 107 | } 108 | } 109 | } 110 | return count, nil 111 | } 112 | 113 | func (c ApplyClient) UpdateAll(ctx context.Context) (int, error) { 114 | secretList, err := c.Client.CoreV1().Secrets("").List(ctx, metav1.ListOptions{}) 115 | if err != nil { 116 | return 0, err 117 | } 118 | count := 0 119 | for _, secret := range secretList.Items { 120 | log.Debugf("Reading secret %v/%v", secret.Namespace, secret.Name) 121 | update, err := c.updateSingleSecret(ctx, secret.Namespace, &secret) 122 | if err != nil { 123 | return count, err 124 | } 125 | if update { 126 | count = count + 1 127 | } 128 | } 129 | return count, nil 130 | } 131 | 132 | func (c ApplyClient) UpdateAllFromNamespace(ctx context.Context) (int, error) { 133 | secretList, err := c.Client.CoreV1().Secrets(c.Options.Namespace).List(ctx, metav1.ListOptions{}) 134 | if err != nil { 135 | return 0, err 136 | } 137 | count := 0 138 | for _, secret := range secretList.Items { 139 | log.Debugf("Reading secret %v/%v", secret.Namespace, secret.Name) 140 | update, err := c.updateSingleSecret(ctx, c.Options.Namespace, &secret) 141 | if err != nil { 142 | return count, err 143 | } 144 | if update { 145 | count = count + 1 146 | } 147 | } 148 | return count, nil 149 | } 150 | 151 | func Root(ctx context.Context, client *ApplyClient, secrets []string) error { 152 | var count int 153 | var err error 154 | if client.Options.AllSecrets && client.Options.AllNamespaces { 155 | count, err = client.UpdateAll(ctx) 156 | } else if client.Options.AllSecrets { 157 | count, err = client.UpdateAllFromNamespace(ctx) 158 | } else if client.Options.AllNamespaces { 159 | count, err = client.UpdateSecretsFromAll(ctx, secrets) 160 | } else { 161 | count, err = client.UpdateSecretsFromNamespace(ctx, secrets) 162 | } 163 | log.Infof("Updated %v secrets", count) 164 | return err 165 | } 166 | -------------------------------------------------------------------------------- /pkg/apply/apply_test.go: -------------------------------------------------------------------------------- 1 | package apply 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | corev1 "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | testclient "k8s.io/client-go/kubernetes/fake" 11 | ) 12 | 13 | func createSecret(secretName string, secretNamespace string, OwnerType string) *corev1.Secret { 14 | secret := corev1.Secret{ 15 | TypeMeta: metav1.TypeMeta{ 16 | Kind: "Secret", 17 | APIVersion: "v1", 18 | }, 19 | ObjectMeta: metav1.ObjectMeta{ 20 | Name: secretName, 21 | Namespace: secretNamespace, 22 | OwnerReferences: []metav1.OwnerReference{ 23 | { 24 | Kind: "ExternalSecret", 25 | APIVersion: OwnerType, 26 | }, 27 | }, 28 | }, 29 | } 30 | return &secret 31 | } 32 | func TestUpdateSecretsFromAll(t *testing.T) { 33 | ctx := context.TODO() 34 | first := createSecret("secret", "one", "right") 35 | second := createSecret("secret", "two", "right") 36 | third := createSecret("secret-2", "two", "right") 37 | fourth := createSecret("secret-2", "one", "right") 38 | fifth := createSecret("fake", "one", "left") 39 | faker := testclient.NewSimpleClientset(first, second, third, fourth, fifth) 40 | options := NewApplyOptions() 41 | options.AllNamespaces = true 42 | options.TargetOwner = "right" 43 | client := ApplyClient{ 44 | Client: faker, 45 | Options: options, 46 | } 47 | targets := []string{"secret"} 48 | count, err := client.UpdateSecretsFromAll(ctx, targets) 49 | assert.Equal(t, 2, count) 50 | assert.NoError(t, err) 51 | targets = []string{"fake"} 52 | count, err = client.UpdateSecretsFromAll(ctx, targets) 53 | assert.NoError(t, err) 54 | assert.Equal(t, 0, count) 55 | } 56 | 57 | func TestUpdateSecretsFromNamespace(t *testing.T) { 58 | ctx := context.TODO() 59 | first := createSecret("secret", "one", "right") 60 | second := createSecret("secret", "two", "right") 61 | third := createSecret("secret-2", "two", "right") 62 | fourth := createSecret("secret-2", "one", "right") 63 | fifth := createSecret("fake", "one", "left") 64 | faker := testclient.NewSimpleClientset(first, second, third, fourth, fifth) 65 | options := NewApplyOptions() 66 | options.AllNamespaces = false 67 | options.Namespace = "one" 68 | options.TargetOwner = "right" 69 | client := ApplyClient{ 70 | Client: faker, 71 | Options: options, 72 | } 73 | targets := []string{"secret"} 74 | count, err := client.UpdateSecretsFromNamespace(ctx, targets) 75 | assert.NoError(t, err) 76 | assert.Equal(t, count, 1) 77 | client.Options.Namespace = "two" 78 | targets = []string{"secret", "secret-2"} 79 | count, err = client.UpdateSecretsFromNamespace(ctx, targets) 80 | assert.NoError(t, err) 81 | assert.Equal(t, count, 2) 82 | targets = []string{"fake"} 83 | count, err = client.UpdateSecretsFromNamespace(ctx, targets) 84 | assert.NoError(t, err) 85 | assert.Equal(t, count, 0) 86 | 87 | } 88 | 89 | func TestUpdateAllFromNamespace(t *testing.T) { 90 | ctx := context.TODO() 91 | first := createSecret("secret", "one", "right") 92 | second := createSecret("secret", "two", "right") 93 | third := createSecret("secret-2", "two", "right") 94 | fourth := createSecret("secret-2", "one", "right") 95 | fifth := createSecret("fake", "one", "left") 96 | faker := testclient.NewSimpleClientset(first, second, third, fourth, fifth) 97 | options := NewApplyOptions() 98 | options.AllNamespaces = false 99 | options.AllSecrets = true 100 | options.Namespace = "one" 101 | options.TargetOwner = "right" 102 | client := ApplyClient{ 103 | Client: faker, 104 | Options: options, 105 | } 106 | count, err := client.UpdateAllFromNamespace(ctx) 107 | assert.NoError(t, err) 108 | assert.Equal(t, count, 2) 109 | client.Options.Namespace = "two" 110 | count, err = client.UpdateAllFromNamespace(ctx) 111 | assert.NoError(t, err) 112 | assert.Equal(t, count, 2) 113 | client.Options.Namespace = "two" 114 | count, err = client.UpdateAllFromNamespace(ctx) 115 | assert.NoError(t, err) 116 | assert.Equal(t, count, 0) // secrets already updated 117 | } 118 | 119 | func TestUpdateAll(t *testing.T) { 120 | ctx := context.TODO() 121 | first := createSecret("secret", "one", "right") 122 | second := createSecret("secret", "two", "right") 123 | third := createSecret("secret-2", "two", "right") 124 | fourth := createSecret("secret-2", "one", "right") 125 | fifth := createSecret("fake", "one", "left") 126 | faker := testclient.NewSimpleClientset(first, second, third, fourth, fifth) 127 | options := NewApplyOptions() 128 | options.AllNamespaces = true 129 | options.AllSecrets = true 130 | options.TargetOwner = "right" 131 | client := ApplyClient{ 132 | Client: faker, 133 | Options: options, 134 | } 135 | count, err := client.UpdateAll(ctx) 136 | assert.NoError(t, err) 137 | assert.Equal(t, count, 4) 138 | count, err = client.UpdateAll(ctx) 139 | assert.NoError(t, err) 140 | assert.Equal(t, count, 0) // secrets already updated 141 | } 142 | 143 | func TestRoot(t *testing.T) { 144 | ctx := context.TODO() 145 | first := createSecret("secret", "one", "right") 146 | second := createSecret("secret", "two", "right") 147 | third := createSecret("secret-2", "two", "right") 148 | fourth := createSecret("secret-2", "one", "right") 149 | fifth := createSecret("fake", "one", "left") 150 | sixth := createSecret("secret", "three", "right") 151 | seventh := createSecret("secret", "four", "right") 152 | eigth := createSecret("secret-2", "four", "right") 153 | nineth := createSecret("secret-2", "three", "right") 154 | tenth := createSecret("fake", "three", "left") 155 | faker := testclient.NewSimpleClientset(first, second, third, fourth, fifth, sixth, seventh, eigth, nineth, tenth) 156 | options := NewApplyOptions() 157 | options.AllNamespaces = true 158 | options.AllSecrets = true 159 | client := ApplyClient{ 160 | Client: faker, 161 | Options: options, 162 | } 163 | targets := []string{"secret"} 164 | err := Root(ctx, &client, targets) 165 | assert.NoError(t, err) 166 | client.Options.AllNamespaces = false 167 | err = Root(ctx, &client, targets) 168 | assert.NoError(t, err) 169 | client.Options.AllSecrets = false 170 | err = Root(ctx, &client, targets) 171 | assert.NoError(t, err) 172 | client.Options.AllNamespaces = true 173 | err = Root(ctx, &client, targets) 174 | assert.NoError(t, err) 175 | } 176 | -------------------------------------------------------------------------------- /pkg/parser/parser.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "kestoeso/pkg/apis" 8 | "kestoeso/pkg/provider" 9 | "kestoeso/pkg/utils" 10 | "math/rand" 11 | "os" 12 | "path/filepath" 13 | "reflect" 14 | "strings" 15 | 16 | api "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1" 17 | log "github.com/sirupsen/logrus" 18 | corev1 "k8s.io/api/core/v1" 19 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 20 | 21 | // "k8s.io/client-go/util/homedir" 22 | // "k8s.io/client-go/kubernetes" 23 | // "k8s.io/client-go/rest" 24 | // "k8s.io/client-go/tools/clientcmd" 25 | 26 | yaml "sigs.k8s.io/yaml" 27 | ) 28 | 29 | // Store DB Functions 30 | 31 | type SecretStoreDB []api.SecretStore 32 | type StoreDB interface { 33 | Exists(S api.SecretStore) (bool, int) 34 | } 35 | 36 | func (storedb SecretStoreDB) Exists(S api.SecretStore) (bool, int) { 37 | for idx, secretStore := range storedb { 38 | if S.Kind == "SecretStore" && 39 | secretStore.Namespace == S.Namespace && 40 | secretStore.APIVersion == S.APIVersion && 41 | secretStore.Kind == S.Kind && 42 | reflect.DeepEqual(secretStore.Spec, S.Spec) { 43 | return true, idx 44 | } else if S.Kind == "ClusterSecretStore" && 45 | secretStore.APIVersion == S.APIVersion && 46 | secretStore.Kind == S.Kind && 47 | reflect.DeepEqual(secretStore.Spec, S.Spec) { 48 | return true, idx 49 | } 50 | } 51 | return false, -1 52 | } 53 | 54 | var ESOSecretStoreList = make(SecretStoreDB, 0) 55 | 56 | // 57 | 58 | func readKESFromFile(file string) (apis.KESExternalSecret, error) { 59 | dat, err := os.ReadFile(file) 60 | if err != nil { 61 | return apis.KESExternalSecret{}, err 62 | } 63 | var K = apis.KESExternalSecret{} 64 | err = yaml.Unmarshal(dat, &K) 65 | if err != nil { 66 | return apis.KESExternalSecret{}, err 67 | } 68 | return K, nil 69 | } 70 | 71 | //TODO: Allow future versions here 72 | func NewESOSecret() api.ExternalSecret { 73 | d := api.ExternalSecret{} 74 | d.TypeMeta = metav1.TypeMeta{ 75 | Kind: "ExternalSecret", 76 | APIVersion: "external-secrets.io/v1alpha1", 77 | } 78 | return d 79 | } 80 | 81 | var letters = []rune("abcdefghijklmnopqrstuvwxyz") 82 | 83 | func randSeq(n int) string { 84 | b := make([]rune, n) 85 | for i := range b { 86 | b[i] = letters[rand.Intn(len(letters))] 87 | } 88 | return string(b) 89 | } 90 | 91 | func mapLoop(m map[string]interface{}) error { 92 | for k := range m { 93 | if k != "metadata" && k != "type" && k != "data" { 94 | return fmt.Errorf("%v templating is currently not supported", k) 95 | } 96 | } 97 | return nil 98 | } 99 | 100 | func canMigrateKes(K apis.KESExternalSecret) error { 101 | err := mapLoop(K.Spec.Template) 102 | if err != nil { 103 | return err 104 | } 105 | for _, data := range K.Spec.Data { 106 | if data.Path != "" { 107 | return errors.New("externalSecret with path selection is currently not supported") 108 | } 109 | } 110 | return nil 111 | } 112 | 113 | func bindProvider(ctx context.Context, S api.SecretStore, K apis.KESExternalSecret, client *provider.KesToEsoClient) (api.SecretStore, bool) { 114 | if client.Options.TargetNamespace != "" { 115 | S.ObjectMeta.Namespace = client.Options.TargetNamespace 116 | } else { 117 | S.ObjectMeta.Namespace = K.ObjectMeta.Namespace 118 | } 119 | var err error 120 | backend := K.Spec.BackendType 121 | switch backend { 122 | case "secretsManager": 123 | p := api.AWSProvider{} 124 | p.Service = api.AWSServiceSecretsManager 125 | p.Role = K.Spec.RoleArn 126 | p.Region = K.Spec.Region 127 | prov := api.SecretStoreProvider{} 128 | prov.AWS = &p 129 | S.Spec.Provider = &prov 130 | S, err = client.InstallAWSSecrets(ctx, S) 131 | if err != nil { 132 | log.Warnf("Failed to Install AWS Backend Specific configuration: %v. Make sure you have set up Controller Pod Identity or manually edit SecretStore before applying it", err) 133 | } 134 | case "systemManager": 135 | p := api.AWSProvider{} 136 | p.Service = api.AWSServiceParameterStore 137 | prov := api.SecretStoreProvider{} 138 | prov.AWS = &p 139 | p.Role = K.Spec.RoleArn 140 | p.Region = K.Spec.Region 141 | S.Spec.Provider = &prov 142 | S, err = client.InstallAWSSecrets(ctx, S) 143 | if err != nil { 144 | log.Warnf("Failed to Install AWS Backend Specific configuration: %v. Make sure you have set up Controller Pod Identity Manually Edit SecretStore before applying it", err) 145 | } 146 | case "azureKeyVault": // TODO RECHECK MAPPING ON REAL USE CASE. WHAT KEYVAULTNAME IS USED FOR? 147 | p := api.AzureKVProvider{} 148 | prov := api.SecretStoreProvider{} 149 | prov.AzureKV = &p 150 | S.Spec.Provider = &prov 151 | vaultUrl := fmt.Sprintf("https://%v.vault.azure.net", K.Spec.KeyVaultName) 152 | S.Spec.Provider.AzureKV.VaultURL = &vaultUrl 153 | S, err = client.InstallAzureKVSecrets(ctx, S) 154 | if err != nil { 155 | log.Warnf("Failed to Install Azure Backend Specific configuration: %v. Manually Edit SecretStore before applying it", err) 156 | } 157 | case "gcpSecretsManager": 158 | p := api.GCPSMProvider{} 159 | p.ProjectID = K.Spec.ProjectID 160 | prov := api.SecretStoreProvider{} 161 | prov.GCPSM = &p 162 | S.Spec.Provider = &prov 163 | S, err = client.InstallGCPSMSecrets(ctx, S) 164 | if err != nil { 165 | log.Warnf("Failed to Install GCP Backend Specific configuration: %v. Makesure you have set up workload identity or manually edit SecretStore before applying it", err) 166 | } 167 | case "ibmcloudSecretsManager": 168 | prov := api.SecretStoreProvider{} 169 | prov.IBM = &api.IBMProvider{} 170 | S.Spec.Provider = &prov 171 | S, err = client.InstallIBMSecrets(ctx, S) 172 | if err != nil { 173 | log.Warnf("Failed to Install IBM Backend Specific configuration: %v. Manually Edit SecretStore before applying it", err) 174 | } 175 | case "vault": // TODO RECHECK MAPPING ON REAL USE CASE 176 | p := api.VaultProvider{} 177 | if K.Spec.KvVersion == 1 { 178 | p.Version = api.VaultKVStoreV1 179 | } else { 180 | p.Version = api.VaultKVStoreV2 181 | p.Path = getVaultProviderPath(K.Spec.Data, K.Spec.DataFrom) 182 | if p.Path == "" { 183 | return S, false 184 | } 185 | } 186 | prov := api.SecretStoreProvider{} 187 | prov.Vault = &p 188 | S.Spec.Provider = &prov 189 | S, err = client.InstallVaultSecrets(ctx, S) 190 | if err != nil { 191 | log.Warnf("Failed to Install Vault Backend Specific configuration: %v. Manually Edit SecretStore before applying it", err) 192 | kubeauth := api.VaultKubernetesAuth{} 193 | S.Spec.Provider.Vault.Auth.Kubernetes = &kubeauth 194 | } 195 | if K.Spec.VaultMountPoint != "" { 196 | S.Spec.Provider.Vault.Auth.Kubernetes.Path = K.Spec.VaultMountPoint 197 | } 198 | if K.Spec.VaultRole != "" { 199 | S.Spec.Provider.Vault.Auth.Kubernetes.Role = K.Spec.VaultRole 200 | } 201 | default: 202 | log.Warnf("Provider %v is not currently supported!", backend) 203 | } 204 | exists, pos := ESOSecretStoreList.Exists(S) 205 | if !exists { 206 | S.ObjectMeta.Name = fmt.Sprintf("%v-secretstore-autogen-%v", strings.ToLower(backend), randSeq(8)) 207 | ESOSecretStoreList = append(ESOSecretStoreList, S) 208 | return S, true 209 | } else { 210 | return ESOSecretStoreList[pos], false 211 | } 212 | } 213 | 214 | func getVaultProviderPath(data []apis.KESExternalSecretData, dataFrom []string) string { 215 | prefix := "" 216 | for _, d := range data { 217 | if prefix == "" { 218 | prefix = strings.Split(d.Key, "/")[0] 219 | } 220 | if prefix != strings.Split(d.Key, "/")[0] { 221 | log.Fatal("Failed to parse secret store for KES secret!") 222 | return "" 223 | } 224 | } 225 | for _, d := range dataFrom { 226 | if prefix == "" { 227 | prefix = strings.Split(d, "/")[0] 228 | } 229 | if prefix != strings.Split(d, "/")[0] { 230 | log.Fatal("Failed to parse secret store for KES secret!") 231 | return "" 232 | } 233 | } 234 | return prefix 235 | } 236 | 237 | func parseSpecifics(K apis.KESExternalSecret, E api.ExternalSecret) (api.ExternalSecret, error) { 238 | backend := K.Spec.BackendType 239 | ans := E 240 | switch backend { 241 | case "vault": 242 | if K.Spec.KvVersion == 2 { 243 | for idx, data := range ans.Spec.Data { 244 | paths := strings.Split(data.RemoteRef.Key, "/") 245 | if paths[1] != "data" { // we have the good format like /data/// 246 | return E, errors.New("secret key not compatible with kv2 format (/data///)") 247 | } 248 | str := strings.Join(paths[2:], "/") 249 | ans.Spec.Data[idx].RemoteRef.Key = str 250 | } 251 | } 252 | for idx, data := range ans.Spec.Data { 253 | if data.RemoteRef.Property == "" { 254 | ans.Spec.Data[idx].RemoteRef.Property = ans.Spec.Data[idx].SecretKey 255 | } 256 | } 257 | for idx, dataFrom := range ans.Spec.DataFrom { 258 | paths := strings.Split(dataFrom.Key, "/") 259 | if paths[1] != "data" { // we have the good format like /data/// 260 | return E, errors.New("secret key not compatible with kv2 format (/data///)") 261 | } 262 | str := strings.Join(paths[2:], "/") 263 | ans.Spec.DataFrom[idx].Key = str 264 | 265 | } 266 | default: 267 | } 268 | return ans, nil 269 | } 270 | 271 | func parseGenerals(K apis.KESExternalSecret, E api.ExternalSecret, options *apis.KesToEsoOptions) (api.ExternalSecret, error) { 272 | secret := E 273 | secret.ObjectMeta.Name = K.ObjectMeta.Name 274 | secret.Spec.Target.Name = K.ObjectMeta.Name // Inherits default in KES, so we should do the same approach here 275 | if options.TargetNamespace != "" { 276 | secret.ObjectMeta.Namespace = options.TargetNamespace 277 | } else { 278 | secret.ObjectMeta.Namespace = K.ObjectMeta.Namespace 279 | } 280 | var refKey string 281 | for _, kesSecretData := range K.Spec.Data { 282 | if kesSecretData.SecretType != "" { 283 | refKey = kesSecretData.SecretType + "/" + kesSecretData.Key 284 | } else { 285 | refKey = kesSecretData.Key 286 | } 287 | esoRemoteRef := api.ExternalSecretDataRemoteRef{ 288 | Key: refKey, 289 | Property: kesSecretData.Property, 290 | Version: kesSecretData.Version} 291 | esoSecretData := api.ExternalSecretData{ 292 | SecretKey: kesSecretData.Name, 293 | RemoteRef: esoRemoteRef} 294 | secret.Spec.Data = append(secret.Spec.Data, esoSecretData) 295 | } 296 | for _, kesSecretDataFrom := range K.Spec.DataFrom { 297 | esoDataFrom := api.ExternalSecretDataRemoteRef{ 298 | Key: kesSecretDataFrom, 299 | } 300 | secret.Spec.DataFrom = append(secret.Spec.DataFrom, esoDataFrom) 301 | } 302 | templ, err := fillTemplate(secret.Spec.Target.Template, K.Spec.Template) 303 | if err != nil { 304 | return secret, err 305 | } 306 | secret.Spec.Target.Template = &templ 307 | return secret, nil 308 | 309 | } 310 | 311 | func fillTemplate(template *api.ExternalSecretTemplate, m map[string]interface{}) (api.ExternalSecretTemplate, error) { 312 | tm := api.ExternalSecretTemplateMetadata{} 313 | ans := api.ExternalSecretTemplate{} 314 | if template != nil { 315 | ans = *template 316 | } 317 | v, ok := m["type"] 318 | if ok { 319 | ans.Type = corev1.SecretType(v.(string)) 320 | } 321 | v, ok = m["data"] 322 | if ok { 323 | ans.Data = make(map[string]string) 324 | metadata, ok := v.(map[string]interface{}) 325 | if ok { 326 | for k, v := range metadata { 327 | ans.Data[k] = v.(string) 328 | } 329 | } 330 | } 331 | v, ok = m["metadata"] 332 | if ok { 333 | n, ok := v.(map[string]interface{}) 334 | if ok { 335 | annot, okann := n["annotations"] 336 | if okann { 337 | tm.Annotations = make(map[string]string) 338 | meta, ok := annot.(map[string]interface{}) 339 | if ok { 340 | for k, v := range meta { 341 | tm.Annotations[k] = v.(string) 342 | } 343 | } 344 | } 345 | label, oklab := n["labels"] 346 | if oklab { 347 | tm.Labels = make(map[string]string) 348 | meta, ok := label.(map[string]interface{}) 349 | if ok { 350 | for k, v := range meta { 351 | tm.Labels[k] = v.(string) 352 | } 353 | } 354 | } 355 | } 356 | } 357 | ans.Metadata = api.ExternalSecretTemplateMetadata{} 358 | ans.Metadata = tm 359 | return ans, nil 360 | } 361 | func linkSecretStore(E api.ExternalSecret, S api.SecretStore) api.ExternalSecret { 362 | ext := E 363 | ext.Spec.SecretStoreRef.Name = S.ObjectMeta.Name 364 | ext.Spec.SecretStoreRef.Kind = S.TypeMeta.Kind 365 | return ext 366 | } 367 | 368 | type RootResponse struct { 369 | Path string 370 | Kes apis.KESExternalSecret 371 | Es api.ExternalSecret 372 | Ss api.SecretStore 373 | } 374 | 375 | func Root(ctx context.Context, client *provider.KesToEsoClient) []RootResponse { 376 | ans := make([]RootResponse, 0) 377 | var files []string 378 | err := filepath.Walk(client.Options.InputPath, func(path string, info os.FileInfo, err error) error { 379 | if !info.IsDir() { 380 | files = append(files, path) 381 | } 382 | return nil 383 | }) 384 | if err != nil { 385 | log.Fatal(err) 386 | } 387 | for _, file := range files { 388 | log.Debugln("Looking for ", file) 389 | K, err := readKESFromFile(file) 390 | if err != nil { 391 | panic(err) 392 | } 393 | if !utils.IsKES(K) { 394 | log.Errorf("Not a KES File: %v\n", file) 395 | continue 396 | } 397 | err = canMigrateKes(K) 398 | if err != nil { 399 | log.Errorf("Cannot process file %v, %v. Skipping", file, err) 400 | continue 401 | } 402 | E, err := parseGenerals(K, NewESOSecret(), client.Options) 403 | if err != nil { 404 | log.Errorf("Could not process file %v: %v. Skipping.", file, err) 405 | continue 406 | } 407 | E, err = parseSpecifics(K, E) 408 | if err != nil { 409 | log.Errorf("Could not process file %v: %v. Skipping.", file, err) 410 | continue 411 | } 412 | S := utils.NewSecretStore(client.Options.SecretStore) 413 | S, newProvider := bindProvider(ctx, S, K, client) 414 | secret_filename := fmt.Sprintf("%v/external-secret-%v.yaml", client.Options.OutputPath, E.ObjectMeta.Name) 415 | if newProvider { 416 | store_filename := fmt.Sprintf("%v/secret-store-%v.yaml", client.Options.OutputPath, S.ObjectMeta.Name) 417 | err = utils.WriteYaml(S, store_filename, client.Options.ToStdout) 418 | if err != nil { 419 | panic(err) 420 | } 421 | } 422 | E = linkSecretStore(E, S) 423 | err = utils.WriteYaml(E, secret_filename, client.Options.ToStdout) 424 | if err != nil { 425 | panic(err) 426 | } 427 | response := RootResponse{ 428 | Path: file, 429 | Kes: K, 430 | Es: E, 431 | Ss: S, 432 | } 433 | ans = append(ans, response) 434 | } 435 | return ans 436 | } 437 | 438 | // Functions for kubernetes application management 439 | -------------------------------------------------------------------------------- /pkg/parser/parser_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "kestoeso/pkg/apis" 7 | "kestoeso/pkg/provider" 8 | "kestoeso/pkg/utils" 9 | "os" 10 | "reflect" 11 | "testing" 12 | 13 | api "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1" 14 | "github.com/stretchr/testify/assert" 15 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 | testclient "k8s.io/client-go/kubernetes/fake" 17 | yaml "sigs.k8s.io/yaml" 18 | ) 19 | 20 | func TestNewEsoSecret(t *testing.T) { 21 | S := NewESOSecret() 22 | if S.TypeMeta.Kind != "ExternalSecret" { 23 | t.Errorf("want ExternalSecret got %v", S.TypeMeta.Kind) 24 | } 25 | if S.TypeMeta.APIVersion != "external-secrets.io/v1alpha1" { 26 | t.Errorf("want external-secrets.io/v1alpha1 got %v", S.TypeMeta.APIVersion) 27 | } 28 | } 29 | 30 | func TestBindAWSSMProvider(t *testing.T) { 31 | ctx := context.TODO() 32 | K := apis.KESExternalSecret{ 33 | Kind: "ExternalSecret", 34 | ApiVersion: "kubernetes-client.io/v1", 35 | ObjectMeta: metav1.ObjectMeta{ 36 | Name: "aws-secretsmanager", 37 | Namespace: "kes-ns", 38 | }, 39 | Spec: apis.KESExternalSecretSpec{ 40 | BackendType: "secretsManager", 41 | VaultMountPoint: "", 42 | VaultRole: "", 43 | ProjectID: "", 44 | RoleArn: "arn:aws:iam::123412341234:role/let-other-account-access-secrets", 45 | Region: "eu-west-1", 46 | DataFrom: []string{ 47 | "path/to/data", 48 | }, 49 | Data: []apis.KESExternalSecretData{ 50 | { 51 | Key: "demo-service/credentials", 52 | Name: "password", 53 | SecretType: "", 54 | Property: "password", 55 | Recursive: "", 56 | Path: "", 57 | VersionStage: "", 58 | IsBinary: false, 59 | }, 60 | { 61 | Key: "demo-service/credentials", 62 | Name: "username", 63 | SecretType: "", 64 | Property: "username", 65 | Recursive: "", 66 | Path: "", 67 | VersionStage: "", 68 | IsBinary: false, 69 | }, 70 | }, 71 | }, 72 | } 73 | S := utils.NewSecretStore(false) 74 | want := utils.NewSecretStore(false) 75 | p := api.AWSProvider{} 76 | p.Service = api.AWSServiceSecretsManager 77 | p.Role = "arn:aws:iam::123412341234:role/let-other-account-access-secrets" 78 | p.Region = "eu-west-1" 79 | prov := api.SecretStoreProvider{} 80 | want.ObjectMeta.Namespace = "kes-ns" 81 | prov.AWS = &p 82 | want.Spec.Provider = &prov 83 | faker := testclient.NewSimpleClientset() 84 | c := provider.KesToEsoClient{ 85 | Client: faker, 86 | Options: &apis.KesToEsoOptions{}, 87 | } 88 | got, _ := bindProvider(ctx, S, K, &c) 89 | // Forcing name to be equal, since it's randomly generated 90 | want.ObjectMeta.Name = got.ObjectMeta.Name 91 | if !reflect.DeepEqual(want, got) { 92 | t.Errorf("want %v got %v", want, got) 93 | } 94 | 95 | } 96 | 97 | func TestBindAWSPSProvider(t *testing.T) { 98 | ctx := context.TODO() 99 | K := apis.KESExternalSecret{ 100 | Kind: "ExternalSecret", 101 | ApiVersion: "kubernetes-client.io/v1", 102 | ObjectMeta: metav1.ObjectMeta{ 103 | Name: "aws-secretsmanager", 104 | Namespace: "kes-ns", 105 | }, 106 | Spec: apis.KESExternalSecretSpec{ 107 | BackendType: "systemManager", 108 | VaultMountPoint: "", 109 | VaultRole: "", 110 | ProjectID: "", 111 | RoleArn: "arn:aws:iam::123412341234:role/let-other-account-access-secrets", 112 | Region: "eu-west-1", 113 | DataFrom: []string{ 114 | "path/to/data", 115 | }, 116 | Data: []apis.KESExternalSecretData{ 117 | { 118 | Key: "demo-service/credentials", 119 | Name: "password", 120 | SecretType: "", 121 | Property: "password", 122 | Recursive: "", 123 | Path: "", 124 | VersionStage: "", 125 | IsBinary: false, 126 | }, 127 | { 128 | Key: "demo-service/credentials", 129 | Name: "username", 130 | SecretType: "", 131 | Property: "username", 132 | Recursive: "", 133 | Path: "", 134 | VersionStage: "", 135 | IsBinary: false, 136 | }, 137 | }, 138 | }, 139 | } 140 | S := utils.NewSecretStore(false) 141 | want := utils.NewSecretStore(false) 142 | p := api.AWSProvider{} 143 | p.Service = api.AWSServiceParameterStore 144 | p.Role = "arn:aws:iam::123412341234:role/let-other-account-access-secrets" 145 | p.Region = "eu-west-1" 146 | prov := api.SecretStoreProvider{} 147 | want.ObjectMeta.Namespace = "kes-ns" 148 | prov.AWS = &p 149 | want.Spec.Provider = &prov 150 | faker := testclient.NewSimpleClientset() 151 | c := provider.KesToEsoClient{ 152 | Client: faker, 153 | Options: &apis.KesToEsoOptions{}, 154 | } 155 | got, _ := bindProvider(ctx, S, K, &c) 156 | // Forcing name to be equal, since it's randomly generated 157 | want.ObjectMeta.Name = got.ObjectMeta.Name 158 | if !reflect.DeepEqual(want, got) { 159 | t.Errorf("want %v got %v", want, got) 160 | } 161 | 162 | } 163 | 164 | func TestBindGCPProvider(t *testing.T) { 165 | ctx := context.TODO() 166 | K := apis.KESExternalSecret{ 167 | Kind: "ExternalSecret", 168 | ApiVersion: "kubernetes-client.io/v1", 169 | ObjectMeta: metav1.ObjectMeta{ 170 | Name: "aws-secretsmanager", 171 | Namespace: "kes-ns", 172 | }, 173 | Spec: apis.KESExternalSecretSpec{ 174 | BackendType: "gcpSecretsManager", 175 | ProjectID: "my-project", 176 | DataFrom: []string{ 177 | "path/to/data", 178 | }, 179 | Data: []apis.KESExternalSecretData{ 180 | { 181 | Key: "kv/demo-service/credentials", 182 | Name: "password", 183 | SecretType: "", 184 | Property: "password", 185 | Recursive: "", 186 | Path: "", 187 | VersionStage: "", 188 | IsBinary: false, 189 | }, 190 | { 191 | Key: "kv/demo-service/credentials", 192 | Name: "username", 193 | SecretType: "", 194 | Property: "username", 195 | Recursive: "", 196 | Path: "", 197 | VersionStage: "", 198 | IsBinary: false, 199 | }, 200 | }, 201 | }, 202 | } 203 | S := utils.NewSecretStore(false) 204 | want := utils.NewSecretStore(false) 205 | p := api.GCPSMProvider{} 206 | p.ProjectID = "my-project" 207 | prov := api.SecretStoreProvider{} 208 | want.ObjectMeta.Namespace = "kes-ns" 209 | prov.GCPSM = &p 210 | want.Spec.Provider = &prov 211 | faker := testclient.NewSimpleClientset() 212 | c := provider.KesToEsoClient{ 213 | Client: faker, 214 | Options: &apis.KesToEsoOptions{}, 215 | } 216 | got, _ := bindProvider(ctx, S, K, &c) 217 | // Forcing name to be equal, since it's randomly generated 218 | want.ObjectMeta.Name = got.ObjectMeta.Name 219 | if !reflect.DeepEqual(want, got) { 220 | t.Errorf("want %v got %v", want, got) 221 | } 222 | 223 | } 224 | 225 | func TestBindIBMProvider(t *testing.T) { 226 | ctx := context.TODO() 227 | K := apis.KESExternalSecret{ 228 | Kind: "ExternalSecret", 229 | ApiVersion: "kubernetes-client.io/v1", 230 | ObjectMeta: metav1.ObjectMeta{ 231 | Name: "aws-secretsmanager", 232 | Namespace: "kes-ns", 233 | }, 234 | Spec: apis.KESExternalSecretSpec{ 235 | BackendType: "ibmcloudSecretsManager", 236 | DataFrom: []string{ 237 | "path/to/data", 238 | }, 239 | Data: []apis.KESExternalSecretData{ 240 | { 241 | Key: "demo-service/credentials", 242 | Name: "password", 243 | SecretType: "username_password", 244 | Property: "password", 245 | Recursive: "", 246 | Path: "", 247 | VersionStage: "", 248 | IsBinary: false, 249 | }, 250 | { 251 | Key: "demo-service/credentials", 252 | Name: "username", 253 | SecretType: "username_password", 254 | Property: "username", 255 | Recursive: "", 256 | Path: "", 257 | VersionStage: "", 258 | IsBinary: false, 259 | }, 260 | }, 261 | }, 262 | } 263 | S := utils.NewSecretStore(false) 264 | want := utils.NewSecretStore(false) 265 | p := api.IBMProvider{} 266 | prov := api.SecretStoreProvider{} 267 | want.ObjectMeta.Namespace = "kes-ns" 268 | prov.IBM = &p 269 | want.Spec.Provider = &prov 270 | faker := testclient.NewSimpleClientset() 271 | c := provider.KesToEsoClient{ 272 | Client: faker, 273 | Options: &apis.KesToEsoOptions{}, 274 | } 275 | got, _ := bindProvider(ctx, S, K, &c) 276 | // Forcing name to be equal, since it's randomly generated 277 | want.ObjectMeta.Name = got.ObjectMeta.Name 278 | if !reflect.DeepEqual(want, got) { 279 | t.Errorf("want %v got %v", want, got) 280 | } 281 | 282 | } 283 | 284 | func TestBindAzureProvider(t *testing.T) { 285 | ctx := context.TODO() 286 | K := apis.KESExternalSecret{ 287 | Kind: "ExternalSecret", 288 | ApiVersion: "kubernetes-client.io/v1", 289 | ObjectMeta: metav1.ObjectMeta{ 290 | Name: "aws-secretsmanager", 291 | Namespace: "kes-ns", 292 | }, 293 | Spec: apis.KESExternalSecretSpec{ 294 | BackendType: "azureKeyVault", 295 | VaultMountPoint: "", 296 | VaultRole: "", 297 | ProjectID: "", 298 | KeyVaultName: "my-vault", 299 | DataFrom: []string{ 300 | "path/to/data", 301 | }, 302 | Data: []apis.KESExternalSecretData{ 303 | { 304 | Key: "demo-service/credentials", 305 | Name: "password", 306 | SecretType: "", 307 | Property: "password", 308 | Recursive: "", 309 | Path: "", 310 | VersionStage: "", 311 | IsBinary: false, 312 | }, 313 | { 314 | Key: "demo-service/credentials", 315 | Name: "username", 316 | SecretType: "", 317 | Property: "username", 318 | Recursive: "", 319 | Path: "", 320 | VersionStage: "", 321 | IsBinary: false, 322 | }, 323 | }, 324 | }, 325 | } 326 | S := utils.NewSecretStore(false) 327 | want := utils.NewSecretStore(false) 328 | p := api.AzureKVProvider{} 329 | url := "https://my-vault.vault.azure.net" 330 | p.VaultURL = &url 331 | prov := api.SecretStoreProvider{} 332 | want.ObjectMeta.Namespace = "kes-ns" 333 | prov.AzureKV = &p 334 | want.Spec.Provider = &prov 335 | faker := testclient.NewSimpleClientset() 336 | c := provider.KesToEsoClient{ 337 | Client: faker, 338 | Options: &apis.KesToEsoOptions{}, 339 | } 340 | got, _ := bindProvider(ctx, S, K, &c) 341 | // Forcing name to be equal, since it's randomly generated 342 | want.ObjectMeta.Name = got.ObjectMeta.Name 343 | if !reflect.DeepEqual(want, got) { 344 | t.Errorf("want %v got %v", want, got) 345 | } 346 | 347 | } 348 | 349 | func TestBindVaultProvider(t *testing.T) { 350 | ctx := context.TODO() 351 | K := apis.KESExternalSecret{ 352 | Kind: "ExternalSecret", 353 | ApiVersion: "kubernetes-client.io/v1", 354 | ObjectMeta: metav1.ObjectMeta{ 355 | Name: "aws-secretsmanager", 356 | Namespace: "kes-ns", 357 | }, 358 | Spec: apis.KESExternalSecretSpec{ 359 | BackendType: "vault", 360 | VaultMountPoint: "kubernetes", 361 | VaultRole: "my-role", 362 | KvVersion: 2, 363 | DataFrom: []string{ 364 | "kv/demo-service/credentials", 365 | }, 366 | Data: []apis.KESExternalSecretData{ 367 | { 368 | Key: "kv/demo-service/credentials", 369 | Name: "password", 370 | SecretType: "", 371 | Property: "password", 372 | Recursive: "", 373 | Path: "", 374 | VersionStage: "", 375 | IsBinary: false, 376 | }, 377 | { 378 | Key: "kv/demo-service/credentials", 379 | Name: "username", 380 | SecretType: "", 381 | Property: "username", 382 | Recursive: "", 383 | Path: "", 384 | VersionStage: "", 385 | IsBinary: false, 386 | }, 387 | }, 388 | }, 389 | } 390 | S := utils.NewSecretStore(false) 391 | want := utils.NewSecretStore(false) 392 | p := api.VaultProvider{} 393 | kubeauth := api.VaultKubernetesAuth{ 394 | Path: "kubernetes", 395 | Role: "my-role", 396 | } 397 | auth := api.VaultAuth{} 398 | p.Version = api.VaultKVStoreV2 399 | p.Path = "kv" 400 | auth.Kubernetes = &kubeauth 401 | p.Auth = auth 402 | prov := api.SecretStoreProvider{} 403 | want.ObjectMeta.Namespace = "kes-ns" 404 | prov.Vault = &p 405 | want.Spec.Provider = &prov 406 | faker := testclient.NewSimpleClientset() 407 | c := provider.KesToEsoClient{ 408 | Client: faker, 409 | Options: &apis.KesToEsoOptions{}, 410 | } 411 | got, _ := bindProvider(ctx, S, K, &c) 412 | // Forcing name to be equal, since it's randomly generated 413 | want.ObjectMeta.Name = got.ObjectMeta.Name 414 | if !reflect.DeepEqual(want, got) { 415 | t.Errorf("want %v got %v", want, got) 416 | } 417 | 418 | } 419 | 420 | func TestParseGenerals(t *testing.T) { 421 | K := apis.KESExternalSecret{ 422 | Kind: "ExternalSecret", 423 | ApiVersion: "kubernetes-client.io/v1", 424 | ObjectMeta: metav1.ObjectMeta{ 425 | Name: "aws-secretsmanager", 426 | Namespace: "default", 427 | }, 428 | Spec: apis.KESExternalSecretSpec{ 429 | BackendType: "secretsManager", 430 | VaultMountPoint: "", 431 | VaultRole: "", 432 | ProjectID: "eu-west-1", 433 | RoleArn: "arn:aws:iam::123412341234:role/let-other-account-access-secrets", 434 | Region: "", 435 | DataFrom: []string{ 436 | "path/to/data", 437 | }, 438 | Data: []apis.KESExternalSecretData{ 439 | { 440 | Key: "demo-service/credentials", 441 | Name: "password", 442 | SecretType: "", 443 | Property: "password", 444 | Recursive: "", 445 | Path: "", 446 | VersionStage: "", 447 | IsBinary: false, 448 | }, 449 | { 450 | Key: "demo-service/credentials", 451 | Name: "username", 452 | SecretType: "", 453 | Property: "username", 454 | Recursive: "", 455 | Path: "", 456 | VersionStage: "", 457 | IsBinary: false, 458 | }, 459 | { 460 | Key: "demo-service/credentials", 461 | Name: "username", 462 | SecretType: "username_password", 463 | Property: "username", 464 | Recursive: "", 465 | Path: "", 466 | VersionStage: "", 467 | IsBinary: false, 468 | }, 469 | }, 470 | }, 471 | } 472 | opt := apis.KesToEsoOptions{} 473 | E := NewESOSecret() 474 | got, err := parseGenerals(K, E, &opt) 475 | if err != nil { 476 | t.Errorf("want success got err: %v", err) 477 | } 478 | 479 | want := api.ExternalSecret{ 480 | ObjectMeta: metav1.ObjectMeta{ 481 | Name: "aws-secretsmanager", 482 | Namespace: "default", 483 | }, 484 | TypeMeta: metav1.TypeMeta{ 485 | Kind: "ExternalSecret", 486 | APIVersion: "external-secrets.io/v1alpha1", 487 | }, 488 | Spec: api.ExternalSecretSpec{ 489 | Target: api.ExternalSecretTarget{ 490 | Name: "aws-secretsmanager", 491 | Template: &api.ExternalSecretTemplate{}, 492 | }, 493 | DataFrom: []api.ExternalSecretDataRemoteRef{ 494 | {Key: "path/to/data"}, 495 | }, 496 | Data: []api.ExternalSecretData{ 497 | { 498 | SecretKey: "password", 499 | RemoteRef: api.ExternalSecretDataRemoteRef{ 500 | Key: "demo-service/credentials", 501 | Property: "password", 502 | }, 503 | }, 504 | { 505 | SecretKey: "username", 506 | RemoteRef: api.ExternalSecretDataRemoteRef{ 507 | Key: "demo-service/credentials", 508 | Property: "username", 509 | }, 510 | }, 511 | { 512 | SecretKey: "username", 513 | RemoteRef: api.ExternalSecretDataRemoteRef{ 514 | Key: "username_password/demo-service/credentials", 515 | Property: "username", 516 | }, 517 | }, 518 | }, 519 | }, 520 | } 521 | if !assert.Equal(t, want, got) { 522 | t.Errorf("want %v got %v - %v x %v", want, got, want.Spec.Target.Template, got.Spec.Target.Template) 523 | } 524 | } 525 | 526 | func TestLinkSecretStore(t *testing.T) { 527 | S := api.SecretStore{ 528 | ObjectMeta: metav1.ObjectMeta{ 529 | Name: "some-secret-store", 530 | }, 531 | TypeMeta: metav1.TypeMeta{ 532 | Kind: "SecretStore", 533 | }, 534 | } 535 | E := NewESOSecret() 536 | got := linkSecretStore(E, S) 537 | want := NewESOSecret() 538 | want.Spec.SecretStoreRef.Name = "some-secret-store" 539 | want.Spec.SecretStoreRef.Kind = "SecretStore" 540 | if !reflect.DeepEqual(got, want) { 541 | t.Errorf("want %v got %v", want, got) 542 | } 543 | } 544 | 545 | func TestParseSpecifics(t *testing.T) { 546 | K := apis.KESExternalSecret{ 547 | Kind: "ExternalSecret", 548 | ApiVersion: "kubernetes-client.io/v1", 549 | ObjectMeta: metav1.ObjectMeta{ 550 | Name: "vault", 551 | Namespace: "default", 552 | }, 553 | Spec: apis.KESExternalSecretSpec{ 554 | BackendType: "vault", 555 | VaultMountPoint: "kubernetes", 556 | VaultRole: "role", 557 | KvVersion: 2, 558 | Region: "", 559 | DataFrom: []string{ 560 | "vault-name/data/path/to/data", 561 | }, 562 | Data: []apis.KESExternalSecretData{ 563 | { 564 | Key: "vault-name/data/demo-service/credentials", 565 | Name: "password", 566 | SecretType: "", 567 | Property: "password", 568 | Recursive: "", 569 | Path: "", 570 | VersionStage: "", 571 | IsBinary: false, 572 | }, 573 | { 574 | Key: "vault-name/data/demo-service/credentials", 575 | Name: "username", 576 | SecretType: "", 577 | Property: "username", 578 | Recursive: "", 579 | Path: "", 580 | VersionStage: "", 581 | IsBinary: false, 582 | }, 583 | }, 584 | }, 585 | } 586 | E := api.ExternalSecret{ 587 | ObjectMeta: metav1.ObjectMeta{ 588 | Name: "vault", 589 | Namespace: "default", 590 | }, 591 | TypeMeta: metav1.TypeMeta{ 592 | Kind: "ExternalSecret", 593 | APIVersion: "external-secrets.io/v1alpha1", 594 | }, 595 | Spec: api.ExternalSecretSpec{ 596 | Target: api.ExternalSecretTarget{ 597 | Name: "vault", 598 | Template: &api.ExternalSecretTemplate{}, 599 | }, 600 | DataFrom: []api.ExternalSecretDataRemoteRef{ 601 | {Key: "vault-name/data/path/to/data"}, 602 | }, 603 | Data: []api.ExternalSecretData{ 604 | { 605 | SecretKey: "password", 606 | RemoteRef: api.ExternalSecretDataRemoteRef{ 607 | Key: "vault-name/data/demo-service/credentials", 608 | Property: "password", 609 | }, 610 | }, 611 | { 612 | SecretKey: "username", 613 | RemoteRef: api.ExternalSecretDataRemoteRef{ 614 | Key: "vault-name/data/demo-service/credentials", 615 | Property: "username", 616 | }, 617 | }, 618 | }, 619 | }, 620 | } 621 | got, err := parseSpecifics(K, E) 622 | if err != nil { 623 | t.Errorf("want success got err: %v", err) 624 | } 625 | 626 | want := api.ExternalSecret{ 627 | ObjectMeta: metav1.ObjectMeta{ 628 | Name: "vault", 629 | Namespace: "default", 630 | }, 631 | TypeMeta: metav1.TypeMeta{ 632 | Kind: "ExternalSecret", 633 | APIVersion: "external-secrets.io/v1alpha1", 634 | }, 635 | Spec: api.ExternalSecretSpec{ 636 | Target: api.ExternalSecretTarget{ 637 | Name: "vault", 638 | Template: &api.ExternalSecretTemplate{}, 639 | }, 640 | DataFrom: []api.ExternalSecretDataRemoteRef{ 641 | {Key: "path/to/data"}, 642 | }, 643 | Data: []api.ExternalSecretData{ 644 | { 645 | SecretKey: "password", 646 | RemoteRef: api.ExternalSecretDataRemoteRef{ 647 | Key: "demo-service/credentials", 648 | Property: "password", 649 | }, 650 | }, 651 | { 652 | SecretKey: "username", 653 | RemoteRef: api.ExternalSecretDataRemoteRef{ 654 | Key: "demo-service/credentials", 655 | Property: "username", 656 | }, 657 | }, 658 | }, 659 | }, 660 | } 661 | if !assert.Equal(t, want, got) { 662 | t.Errorf("want %v got %v", want, got) 663 | } 664 | bad := api.ExternalSecret{ 665 | ObjectMeta: metav1.ObjectMeta{ 666 | Name: "vault", 667 | Namespace: "default", 668 | }, 669 | TypeMeta: metav1.TypeMeta{ 670 | Kind: "ExternalSecret", 671 | APIVersion: "external-secrets.io/v1alpha1", 672 | }, 673 | Spec: api.ExternalSecretSpec{ 674 | Target: api.ExternalSecretTarget{ 675 | Name: "vault", 676 | Template: &api.ExternalSecretTemplate{}, 677 | }, 678 | DataFrom: []api.ExternalSecretDataRemoteRef{ 679 | {Key: "path/to/data"}, 680 | }, 681 | Data: []api.ExternalSecretData{ 682 | { 683 | SecretKey: "password", 684 | RemoteRef: api.ExternalSecretDataRemoteRef{ 685 | Key: "vault-name/demo-service/credentials", 686 | Property: "password", 687 | }, 688 | }, 689 | { 690 | SecretKey: "username", 691 | RemoteRef: api.ExternalSecretDataRemoteRef{ 692 | Key: "vault-name/demo-service/credentials", 693 | Property: "username", 694 | }, 695 | }, 696 | }, 697 | }, 698 | } 699 | _, err = parseSpecifics(K, bad) 700 | if err.Error() != "secret key not compatible with kv2 format (/data///)" { 701 | t.Errorf("want 'secret key not compatible with kv2 format (/data///)' got : %v", err) 702 | } 703 | } 704 | 705 | type rootStruct struct { 706 | name string 707 | golden string 708 | input apis.KESExternalSecret 709 | secretStoreWants *api.SecretStore 710 | clusterSecretStoreWants *api.ClusterSecretStore 711 | externalSecretWants api.ExternalSecret 712 | } 713 | 714 | func loadInput(cases []rootStruct) ([]rootStruct, error) { 715 | ans := cases 716 | for idx, test := range cases { 717 | kes, err := readKESFromFile(fmt.Sprintf("testdata/%v.golden", test.golden)) 718 | if err != nil { 719 | return cases, err 720 | } 721 | ans[idx].input = kes 722 | } 723 | return ans, nil 724 | } 725 | 726 | func readSSFromFile(file string) (api.SecretStore, error) { 727 | dat, err := os.ReadFile(file) 728 | if err != nil { 729 | return api.SecretStore{}, err 730 | } 731 | var K = api.SecretStore{} 732 | err = yaml.Unmarshal(dat, &K) 733 | if err != nil { 734 | return api.SecretStore{}, err 735 | } 736 | return K, nil 737 | } 738 | 739 | func readCSSFromFile(file string) (api.ClusterSecretStore, error) { 740 | dat, err := os.ReadFile(file) 741 | if err != nil { 742 | return api.ClusterSecretStore{}, err 743 | } 744 | var K = api.ClusterSecretStore{} 745 | err = yaml.Unmarshal(dat, &K) 746 | if err != nil { 747 | return api.ClusterSecretStore{}, err 748 | } 749 | return K, nil 750 | } 751 | 752 | func readESFromFile(file string) (api.ExternalSecret, error) { 753 | dat, err := os.ReadFile(file) 754 | if err != nil { 755 | return api.ExternalSecret{}, err 756 | } 757 | var K = api.ExternalSecret{} 758 | err = yaml.Unmarshal(dat, &K) 759 | if err != nil { 760 | return api.ExternalSecret{}, err 761 | } 762 | return K, nil 763 | } 764 | 765 | func loadWants(cases []rootStruct) ([]rootStruct, error) { 766 | ans := cases 767 | for idx, test := range cases { 768 | es, err := readESFromFile(fmt.Sprintf("testdata/es_%v.golden", test.golden)) 769 | if err != nil { 770 | return cases, err 771 | } 772 | ans[idx].externalSecretWants = es 773 | ss, err_ss := readSSFromFile(fmt.Sprintf("testdata/ss_%v.golden", test.golden)) 774 | css, err_css := readCSSFromFile(fmt.Sprintf("testdata/css_%v.golden", test.golden)) 775 | if err_ss != nil && err_css != nil { 776 | return cases, err 777 | } 778 | if err_ss == nil { 779 | ans[idx].secretStoreWants = &ss 780 | } 781 | if err_css == nil { 782 | ans[idx].clusterSecretStoreWants = &css 783 | } 784 | } 785 | return ans, nil 786 | } 787 | func TestRoot(t *testing.T) { 788 | ctx := context.TODO() 789 | testCases := []rootStruct{ 790 | { 791 | name: "aws-secretsmanager", 792 | golden: "aws-secretsmanager", 793 | }, 794 | } 795 | testCases, err := loadInput(testCases) 796 | if err != nil { 797 | t.Fatalf("ERROR! %v", err) 798 | } 799 | _, err = loadWants(testCases) 800 | if err != nil { 801 | t.Fatalf("ERROR! %v", err) 802 | } 803 | options := apis.KesToEsoOptions{ 804 | Namespace: "", 805 | ContainerName: "", 806 | DeploymentName: "", 807 | InputPath: "testdata", 808 | ToStdout: true, 809 | } 810 | faker := testclient.NewSimpleClientset() 811 | c := provider.KesToEsoClient{ 812 | Client: faker, 813 | Options: &options, 814 | } 815 | resp := Root(ctx, &c) 816 | for idx, testcase := range testCases { 817 | assert.Equal(t, testcase.externalSecretWants, resp[idx].Es) 818 | if testcase.secretStoreWants != nil { 819 | assert.Equal(t, *testcase.secretStoreWants, resp[idx].Ss) 820 | assert.Equal(t, testcase.secretStoreWants.Spec.Provider, resp[idx].Ss.Spec.Provider) 821 | } 822 | if testcase.clusterSecretStoreWants != nil { 823 | assert.Equal(t, *testcase.clusterSecretStoreWants, resp[idx].Ss) 824 | assert.Equal(t, testcase.clusterSecretStoreWants.Spec.Provider, resp[idx].Ss.Spec.Provider) 825 | } 826 | } 827 | 828 | } 829 | -------------------------------------------------------------------------------- /pkg/parser/testdata/aws-secretsmanager.golden: -------------------------------------------------------------------------------- 1 | apiVersion: kubernetes-client.io/v1 2 | kind: ExternalSecret 3 | metadata: 4 | name: aws-secretsmanager 5 | spec: 6 | backendType: secretsManager 7 | # optional: specify role to assume when retrieving the data 8 | roleArn: arn:aws:iam::123412341234:role/let-other-account-access-secrets 9 | # optional: specify region of the secret 10 | region: eu-west-1 11 | data: 12 | - key: demo-service/credentials 13 | name: password 14 | property: password 15 | - key: demo-service/credentials 16 | name: username 17 | property: username 18 | -------------------------------------------------------------------------------- /pkg/parser/testdata/es_aws-secretsmanager.golden: -------------------------------------------------------------------------------- 1 | apiVersion: external-secrets.io/v1alpha1 2 | kind: ExternalSecret 3 | metadata: 4 | creationTimestamp: null 5 | name: aws-secretsmanager 6 | spec: 7 | data: 8 | - remoteRef: 9 | key: demo-service/credentials 10 | property: password 11 | secretKey: password 12 | - remoteRef: 13 | key: demo-service/credentials 14 | property: username 15 | secretKey: username 16 | secretStoreRef: 17 | kind: ClusterSecretStore 18 | name: secretsmanager-secretstore-autogen-xvlbzgba 19 | target: 20 | name: aws-secretsmanager 21 | template: 22 | metadata: {} 23 | status: 24 | refreshTime: null 25 | -------------------------------------------------------------------------------- /pkg/parser/testdata/ss_aws-secretsmanager.golden: -------------------------------------------------------------------------------- 1 | apiVersion: external-secrets.io/v1alpha1 2 | kind: ClusterSecretStore 3 | metadata: 4 | creationTimestamp: null 5 | name: secretsmanager-secretstore-autogen-xvlbzgba 6 | namespace: kes-ns 7 | spec: 8 | controller: "" 9 | provider: 10 | aws: 11 | auth: {} 12 | region: eu-west-1 13 | role: arn:aws:iam::123412341234:role/let-other-account-access-secrets 14 | service: SecretsManager 15 | status: 16 | conditions: null 17 | -------------------------------------------------------------------------------- /pkg/provider/provider.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "kestoeso/pkg/apis" 8 | "kestoeso/pkg/utils" 9 | "strings" 10 | 11 | api "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1" 12 | esmeta "github.com/external-secrets/external-secrets/apis/meta/v1" 13 | corev1 "k8s.io/api/core/v1" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/client-go/kubernetes" 16 | ) 17 | 18 | type KesToEsoClient struct { 19 | Options *apis.KesToEsoOptions 20 | Client kubernetes.Interface 21 | } 22 | 23 | func (c KesToEsoClient) GetSecretValue(ctx context.Context, name string, key string, namespace string) (string, error) { 24 | secret, err := c.Client.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{}) 25 | if err != nil { 26 | return "", err 27 | } 28 | value := secret.Data[key] 29 | return string(value), nil 30 | } 31 | 32 | func (c KesToEsoClient) GetServiceAccountIfAnnotationExists(ctx context.Context, key string, sa *esmeta.ServiceAccountSelector) (*corev1.ServiceAccount, error) { 33 | s, err := c.Client.CoreV1().ServiceAccounts(*sa.Namespace).Get(ctx, sa.Name, metav1.GetOptions{}) 34 | if err != nil { 35 | return nil, err 36 | } 37 | _, found := s.Annotations[key] 38 | if found { 39 | return s, nil 40 | } else { 41 | return nil, errors.New("annotation key absent in service account") 42 | } 43 | } 44 | 45 | func (c KesToEsoClient) InstallAWSSecrets(ctx context.Context, S api.SecretStore) (api.SecretStore, error) { 46 | ans := S 47 | deployment, err := c.Client.AppsV1().Deployments(c.Options.Namespace).Get(ctx, c.Options.DeploymentName, metav1.GetOptions{}) 48 | if err != nil { 49 | return S, err 50 | } 51 | containers := deployment.Spec.Template.Spec.Containers 52 | var accessKeyIdSecretKeyRefKey, accessKeyIdSecretKeyRefName string 53 | var secretAccessKeySecretKeyRefKey, secretAccessKeySecretKeyRefName string 54 | var newsecret = &corev1.Secret{} 55 | for _, container := range containers { 56 | if container.Name == c.Options.ContainerName { 57 | containerEnvs := container.Env 58 | for _, env := range containerEnvs { 59 | if env.Name == "AWS_ACCESS_KEY_ID" { 60 | if env.ValueFrom != nil { 61 | accessKeyIdSecretKeyRefName = env.ValueFrom.SecretKeyRef.Name 62 | accessKeyIdSecretKeyRefKey = env.ValueFrom.SecretKeyRef.Key 63 | } else if env.Value != "" { 64 | accessKeyIdSecretKeyRefName = "aws-secrets" 65 | accessKeyIdSecretKeyRefKey = "access-key-id" 66 | ns := c.Options.Namespace 67 | if c.Options.TargetNamespace != "" { 68 | ns = c.Options.TargetNamespace 69 | } 70 | keySelector := esmeta.SecretKeySelector{ 71 | Name: accessKeyIdSecretKeyRefName, 72 | Namespace: &ns, 73 | Key: accessKeyIdSecretKeyRefKey, 74 | } 75 | newsecret, err = utils.UpdateOrCreateSecret(newsecret, &keySelector, env.Value) 76 | if err != nil { 77 | return S, err 78 | } 79 | } 80 | } 81 | if env.Name == "AWS_SECRET_ACCESS_KEY" { 82 | if env.ValueFrom != nil { 83 | secretAccessKeySecretKeyRefName = env.ValueFrom.SecretKeyRef.Name 84 | secretAccessKeySecretKeyRefKey = env.ValueFrom.SecretKeyRef.Key 85 | } else if env.Value != "" { 86 | secretAccessKeySecretKeyRefName = "aws-secrets" 87 | secretAccessKeySecretKeyRefKey = "secret-access-key" 88 | ns := c.Options.Namespace 89 | if c.Options.TargetNamespace != "" { 90 | ns = c.Options.TargetNamespace 91 | } 92 | secretSelector := esmeta.SecretKeySelector{ 93 | Name: secretAccessKeySecretKeyRefName, 94 | Namespace: &ns, 95 | Key: secretAccessKeySecretKeyRefKey, 96 | } 97 | newsecret, err = utils.UpdateOrCreateSecret(newsecret, &secretSelector, env.Value) 98 | if err != nil { 99 | return S, err 100 | } 101 | } 102 | } 103 | if accessKeyIdSecretKeyRefName != "" && secretAccessKeySecretKeyRefName != "" { 104 | break 105 | } 106 | } 107 | break 108 | } 109 | } 110 | awsSecretRef := api.AWSAuthSecretRef{ 111 | AccessKeyID: esmeta.SecretKeySelector{ 112 | Name: accessKeyIdSecretKeyRefName, 113 | Key: accessKeyIdSecretKeyRefKey, 114 | Namespace: &c.Options.Namespace, 115 | }, 116 | SecretAccessKey: esmeta.SecretKeySelector{ 117 | Name: secretAccessKeySecretKeyRefName, 118 | Key: secretAccessKeySecretKeyRefKey, 119 | Namespace: &c.Options.Namespace, 120 | }, 121 | } 122 | ans.Spec.Provider.AWS.Auth.SecretRef = &awsSecretRef 123 | if newsecret.ObjectMeta.Name != "" { 124 | secret_filename := fmt.Sprintf("%v/secret-%v.yaml", c.Options.OutputPath, newsecret.ObjectMeta.Name) 125 | err := utils.WriteYaml(newsecret, secret_filename, c.Options.ToStdout) 126 | if err != nil { 127 | return ans, err 128 | } 129 | } 130 | if awsSecretRef.AccessKeyID.Name == "" || awsSecretRef.SecretAccessKey.Name == "" { 131 | saSelector := esmeta.ServiceAccountSelector{ 132 | Namespace: &c.Options.Namespace, 133 | Name: deployment.Spec.Template.Spec.ServiceAccountName, 134 | } 135 | _, err := c.GetServiceAccountIfAnnotationExists(ctx, "eks.amazonaws.com/role-arn", &saSelector) // Later On with --copy-secret-auths we can use SA to change namespaces and apply 136 | if err != nil { 137 | return S, errors.New("could not find aws credential information (secrets or sa with role-arn annotation) on kes deployment") 138 | } 139 | JWTAuth := api.AWSJWTAuth{ServiceAccountRef: &saSelector} 140 | ans.Spec.Provider.AWS.Auth.JWTAuth = &JWTAuth 141 | } 142 | return ans, nil 143 | } 144 | 145 | func (c KesToEsoClient) InstallVaultSecrets(ctx context.Context, S api.SecretStore) (api.SecretStore, error) { 146 | ans := S 147 | authRef := api.VaultKubernetesAuth{} 148 | deployment, err := c.Client.AppsV1().Deployments(c.Options.Namespace).Get(ctx, c.Options.DeploymentName, metav1.GetOptions{}) 149 | if err != nil { 150 | return S, err 151 | } 152 | newsecret := &corev1.Secret{} 153 | containers := deployment.Spec.Template.Spec.Containers 154 | serviceAccountName := deployment.Spec.Template.Spec.ServiceAccountName 155 | serviceAccountNS := deployment.ObjectMeta.Namespace 156 | serviceAccountRef := esmeta.ServiceAccountSelector{ 157 | Name: serviceAccountName, 158 | Namespace: &serviceAccountNS, 159 | } 160 | authRef.ServiceAccountRef = &serviceAccountRef 161 | for _, container := range containers { 162 | if container.Name == c.Options.ContainerName { 163 | envs := container.Env 164 | for _, env := range envs { 165 | if env.Name == "VAULT_ADDR" { 166 | if env.Value != "" { 167 | ans.Spec.Provider.Vault.Server = env.Value 168 | } else if env.ValueFrom != nil { 169 | key := env.ValueFrom.SecretKeyRef.Key 170 | name := env.ValueFrom.SecretKeyRef.Name 171 | value, err := c.GetSecretValue(ctx, name, key, c.Options.Namespace) 172 | if err != nil { 173 | return S, errors.New("could not find env value for vault_addr") 174 | } 175 | ans.Spec.Provider.Vault.Server = value 176 | } 177 | } 178 | if env.Name == "DEFAULT_VAULT_MOUNT_POINT" { 179 | if env.ValueFrom != nil { 180 | key := env.ValueFrom.SecretKeyRef.Key 181 | name := env.ValueFrom.SecretKeyRef.Name 182 | value, err := c.GetSecretValue(ctx, name, key, c.Options.Namespace) 183 | if err != nil { 184 | return S, errors.New("could not find secret value for default_vault_mount_point") 185 | } 186 | authRef.Path = value 187 | } else if env.Value != "" { 188 | authRef.Path = env.Value 189 | } 190 | } 191 | if env.Name == "DEFAULT_VAULT_ROLE" { 192 | if env.ValueFrom != nil { 193 | key := env.ValueFrom.SecretKeyRef.Key 194 | name := env.ValueFrom.SecretKeyRef.Name 195 | value, err := c.GetSecretValue(ctx, name, key, c.Options.Namespace) 196 | if err != nil { 197 | return S, errors.New("could not find secret value for default_vault_role") 198 | } 199 | authRef.Role = value 200 | } else if env.Value != "" { 201 | authRef.Role = env.Value 202 | } 203 | } 204 | } 205 | } 206 | } 207 | ans.Spec.Provider.Vault.Auth.Kubernetes = &authRef 208 | if newsecret.ObjectMeta.Name != "" { 209 | secret_filename := fmt.Sprintf("%v/secret-vault-provider-%v.yaml", c.Options.OutputPath, newsecret.ObjectMeta.Name) 210 | err := utils.WriteYaml(newsecret, secret_filename, c.Options.ToStdout) 211 | if err != nil { 212 | return ans, err 213 | } 214 | } 215 | if authRef.Role == "" || authRef.Path == "" || ans.Spec.Provider.Vault.Server == "" { 216 | return ans, errors.New("credentials for vault not found in kes deployment") 217 | } 218 | return ans, nil 219 | } 220 | 221 | func (c KesToEsoClient) InstallGCPSMSecrets(ctx context.Context, S api.SecretStore) (api.SecretStore, error) { 222 | ans := S 223 | deployment, err := c.Client.AppsV1().Deployments(c.Options.Namespace).Get(ctx, c.Options.DeploymentName, metav1.GetOptions{}) 224 | if err != nil { 225 | return S, err 226 | } 227 | containers := deployment.Spec.Template.Spec.Containers 228 | volumeName := "" 229 | keyName := "" 230 | for _, container := range containers { 231 | if container.Name == c.Options.ContainerName { 232 | mountPath := "" 233 | containerEnvs := container.Env 234 | for _, env := range containerEnvs { 235 | if env.Name == "GOOGLE_APPLICATION_CREDENTIALS" { 236 | mountPathSlice := strings.Split(env.Value, "/") 237 | for idx, path := range mountPathSlice { 238 | if idx == 0 { 239 | mountPath = path 240 | } else if idx < len(mountPathSlice)-1 { 241 | mountPath = mountPath + "/" + path 242 | } 243 | } 244 | keyName = mountPathSlice[len(mountPathSlice)-1] 245 | } 246 | } 247 | volumeMounts := container.VolumeMounts 248 | for _, mount := range volumeMounts { 249 | if mount.MountPath == mountPath { 250 | volumeName = mount.Name 251 | } 252 | } 253 | } 254 | } 255 | var secretName string 256 | volumes := deployment.Spec.Template.Spec.Volumes 257 | for _, volume := range volumes { 258 | if volume.Name == volumeName { 259 | secretName = volume.Secret.SecretName 260 | ans.Spec.Provider.GCPSM.Auth.SecretRef.SecretAccessKey.Name = secretName 261 | ans.Spec.Provider.GCPSM.Auth.SecretRef.SecretAccessKey.Key = keyName 262 | ans.Spec.Provider.GCPSM.Auth.SecretRef.SecretAccessKey.Namespace = &c.Options.Namespace 263 | } 264 | } 265 | if secretName == "" || keyName == "" { 266 | return ans, errors.New("credentials for gcp sm not found in kes deployment") 267 | } 268 | // if reflect.DeepEqual(ans, S) { 269 | // } 270 | return ans, nil 271 | } 272 | 273 | func (c KesToEsoClient) InstallAzureKVSecrets(ctx context.Context, S api.SecretStore) (api.SecretStore, error) { 274 | ans := S 275 | authRef := api.AzureKVAuth{} 276 | target := c.Options 277 | deployment, err := c.Client.AppsV1().Deployments(target.Namespace).Get(ctx, target.DeploymentName, metav1.GetOptions{}) 278 | if err != nil { 279 | return S, err 280 | } 281 | newsecret := &corev1.Secret{} 282 | containers := deployment.Spec.Template.Spec.Containers 283 | for _, container := range containers { 284 | if container.Name == target.ContainerName { 285 | envs := container.Env 286 | for _, env := range envs { 287 | if env.Name == "AZURE_TENANT_ID" { 288 | if env.Value != "" { 289 | svc := env.Value 290 | ans.Spec.Provider.AzureKV.TenantID = &svc 291 | } else if env.ValueFrom != nil { 292 | key := env.ValueFrom.SecretKeyRef.Key 293 | name := env.ValueFrom.SecretKeyRef.Name 294 | value, err := c.GetSecretValue(ctx, name, key, target.Namespace) 295 | if err != nil { 296 | return S, errors.New("could not find secret value for azure_tenant_id") 297 | } 298 | ans.Spec.Provider.AzureKV.TenantID = &value 299 | } 300 | } 301 | if env.Name == "AZURE_CLIENT_ID" { 302 | ns := c.Options.Namespace 303 | if c.Options.TargetNamespace != "" { 304 | ns = c.Options.TargetNamespace 305 | } 306 | if env.ValueFrom != nil { 307 | key := env.ValueFrom.SecretKeyRef.Key 308 | name := env.ValueFrom.SecretKeyRef.Name 309 | clientSelector := esmeta.SecretKeySelector{ 310 | Name: name, 311 | Key: key, 312 | Namespace: &ns, 313 | } 314 | authRef.ClientID = &clientSelector 315 | } else if env.Value != "" { 316 | clientSelector := esmeta.SecretKeySelector{ 317 | Name: "azure-secrets", 318 | Namespace: &ns, 319 | Key: "client-id", 320 | } 321 | newsecret, err = utils.UpdateOrCreateSecret(newsecret, &clientSelector, env.Value) 322 | if err != nil { 323 | return S, err 324 | } 325 | authRef.ClientID = &clientSelector 326 | } 327 | } 328 | if env.Name == "AZURE_CLIENT_SECRET" { 329 | ns := c.Options.Namespace 330 | if c.Options.TargetNamespace != "" { 331 | ns = c.Options.TargetNamespace 332 | } 333 | if env.ValueFrom != nil { 334 | key := env.ValueFrom.SecretKeyRef.Key 335 | name := env.ValueFrom.SecretKeyRef.Name 336 | secretSelector := esmeta.SecretKeySelector{ 337 | Name: name, 338 | Key: key, 339 | Namespace: &ns, 340 | } 341 | authRef.ClientSecret = &secretSelector 342 | } else if env.Value != "" { 343 | secretSelector := esmeta.SecretKeySelector{ 344 | Name: "azure-secrets", 345 | Namespace: &ns, 346 | Key: "client-secrets", 347 | } 348 | newsecret, err = utils.UpdateOrCreateSecret(newsecret, &secretSelector, env.Value) 349 | if err != nil { 350 | return S, err 351 | } 352 | authRef.ClientSecret = &secretSelector 353 | } 354 | 355 | } 356 | } 357 | } 358 | } 359 | ans.Spec.Provider.AzureKV.AuthSecretRef = &authRef 360 | if newsecret.ObjectMeta.Name != "" { 361 | secret_filename := fmt.Sprintf("%v/secret-azure-provider-%v.yaml", target.OutputPath, newsecret.ObjectMeta.Name) 362 | err := utils.WriteYaml(newsecret, secret_filename, target.ToStdout) 363 | if err != nil { 364 | return ans, err 365 | } 366 | } 367 | if authRef.ClientID == nil || authRef.ClientSecret == nil { 368 | return ans, errors.New("credentials for azure not found in kes deployment") 369 | } 370 | return ans, nil 371 | } 372 | 373 | func (c KesToEsoClient) InstallIBMSecrets(ctx context.Context, S api.SecretStore) (api.SecretStore, error) { 374 | ans := S 375 | authRef := api.IBMAuth{} 376 | deployment, err := c.Client.AppsV1().Deployments(c.Options.Namespace).Get(ctx, c.Options.DeploymentName, metav1.GetOptions{}) 377 | if err != nil { 378 | return S, err 379 | } 380 | newsecret := &corev1.Secret{} 381 | containers := deployment.Spec.Template.Spec.Containers 382 | for _, container := range containers { 383 | if container.Name == c.Options.ContainerName { 384 | envs := container.Env 385 | for _, env := range envs { 386 | if env.Name == "IBM_CLOUD_SECRETS_MANAGER_API_APIKEY" { 387 | if env.Value != "" { 388 | ns := c.Options.Namespace 389 | if c.Options.TargetNamespace != "" { 390 | ns = c.Options.TargetNamespace 391 | } 392 | secretSelector := esmeta.SecretKeySelector{ 393 | Name: "ibm-secrets", 394 | Namespace: &ns, 395 | Key: "api-key", 396 | } 397 | newsecret, err = utils.UpdateOrCreateSecret(newsecret, &secretSelector, env.Value) 398 | if err != nil { 399 | return S, err 400 | } 401 | authRef.SecretRef.SecretAPIKey = secretSelector 402 | } else if env.ValueFrom != nil { 403 | key := env.ValueFrom.SecretKeyRef.Key 404 | name := env.ValueFrom.SecretKeyRef.Name 405 | secretSelector := esmeta.SecretKeySelector{ 406 | Name: name, 407 | Key: key, 408 | Namespace: &c.Options.Namespace, 409 | } 410 | authRef.SecretRef.SecretAPIKey = secretSelector 411 | } 412 | } 413 | if env.Name == "IBM_CLOUD_SECRETS_MANAGER_API_ENDPOINT" { 414 | if env.ValueFrom != nil { 415 | key := env.ValueFrom.SecretKeyRef.Key 416 | name := env.ValueFrom.SecretKeyRef.Name 417 | value, err := c.GetSecretValue(ctx, name, key, c.Options.Namespace) 418 | if err != nil { 419 | return S, errors.New("could not find secret value for ibm_cloud_secrets_manager_api_endpoint") 420 | } 421 | ans.Spec.Provider.IBM.ServiceURL = &value 422 | } else if env.Value != "" { 423 | svc := env.Value 424 | ans.Spec.Provider.IBM.ServiceURL = &svc 425 | } 426 | } 427 | } 428 | } 429 | } 430 | ans.Spec.Provider.IBM.Auth = authRef 431 | if newsecret.ObjectMeta.Name != "" { 432 | secret_filename := fmt.Sprintf("%v/secret-ibm-provider-%v.yaml", c.Options.OutputPath, newsecret.ObjectMeta.Name) 433 | err := utils.WriteYaml(newsecret, secret_filename, c.Options.ToStdout) 434 | if err != nil { 435 | return ans, err 436 | } 437 | } 438 | if authRef.SecretRef.SecretAPIKey.Name == "" { 439 | return ans, errors.New("credentials for ibm cloud not found in kes deployment. edit secretstore definitions before using it") 440 | } 441 | return ans, nil 442 | } 443 | -------------------------------------------------------------------------------- /pkg/provider/provider_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "kestoeso/pkg/apis" 7 | "kestoeso/pkg/utils" 8 | "reflect" 9 | "testing" 10 | 11 | api "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1" 12 | 13 | esmeta "github.com/external-secrets/external-secrets/apis/meta/v1" 14 | appsv1 "k8s.io/api/apps/v1" 15 | corev1 "k8s.io/api/core/v1" 16 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 | testclient "k8s.io/client-go/kubernetes/fake" 18 | ) 19 | 20 | func TestGetSecretValue(t *testing.T) { 21 | ctx := context.TODO() 22 | secret := corev1.Secret{ 23 | TypeMeta: metav1.TypeMeta{ 24 | Kind: "Secret", 25 | APIVersion: "v1", 26 | }, 27 | ObjectMeta: metav1.ObjectMeta{ 28 | Name: "test", 29 | Namespace: "default", 30 | }, 31 | Data: map[string][]byte{ 32 | "key": []byte("secret"), 33 | }, 34 | } 35 | faker := testclient.NewSimpleClientset(&secret) 36 | opt := apis.KesToEsoOptions{} 37 | c := KesToEsoClient{ 38 | Client: faker, 39 | Options: &opt, 40 | } 41 | ans, err := c.GetSecretValue(ctx, "test", "key", "default") 42 | if err != nil { 43 | t.Errorf("want success got %v", err) 44 | } 45 | if ans != "secret" { 46 | t.Errorf("want secret got %v", ans) 47 | } 48 | } 49 | 50 | func TestGetServiceAccount(t *testing.T) { 51 | ctx := context.TODO() 52 | expectSuccess := corev1.ServiceAccount{ 53 | ObjectMeta: metav1.ObjectMeta{ 54 | Name: "sa", 55 | Namespace: "default", 56 | Annotations: map[string]string{ 57 | "a.annotation": "false", 58 | "an.other/annotation": "something", 59 | }, 60 | }, 61 | } 62 | expectFailure := corev1.ServiceAccount{ 63 | ObjectMeta: metav1.ObjectMeta{ 64 | Name: "sa2", 65 | Namespace: "default", 66 | Annotations: map[string]string{ 67 | "b.annotation": "false", 68 | "an.other/annotation": "something", 69 | }, 70 | }, 71 | } 72 | faker := testclient.NewSimpleClientset(&expectSuccess, &expectFailure) 73 | ns := "default" 74 | saSelector := esmeta.ServiceAccountSelector{Name: "sa", Namespace: &ns} 75 | client := KesToEsoClient{ 76 | Client: faker, 77 | Options: &apis.KesToEsoOptions{}, 78 | } 79 | sa, err := client.GetServiceAccountIfAnnotationExists(ctx, "a.annotation", &saSelector) 80 | if err != nil { 81 | t.Errorf("want success got %v", err) 82 | } 83 | if !reflect.DeepEqual(sa, &expectSuccess) { 84 | t.Errorf("want %v got %v", &expectSuccess, sa) 85 | } 86 | saSelector = esmeta.ServiceAccountSelector{Name: "sa2", Namespace: &ns} 87 | _, err = client.GetServiceAccountIfAnnotationExists(ctx, "a.annotation", &saSelector) 88 | if err != nil { 89 | errmsg := fmt.Sprintf("%v", err) 90 | if errmsg != "annotation key absent in service account" { 91 | t.Errorf("Want annotation error got %v", errmsg) 92 | } 93 | } 94 | } 95 | 96 | func TestAWSInstall(t *testing.T) { 97 | ctx := context.TODO() 98 | deploymentWithSecretRef := appsv1.Deployment{ 99 | TypeMeta: metav1.TypeMeta{ 100 | Kind: "Deployment", 101 | APIVersion: "apps/v1", 102 | }, 103 | ObjectMeta: metav1.ObjectMeta{ 104 | Name: "kubernetes-external-secrets", 105 | Namespace: "kes-ns", 106 | }, 107 | Spec: appsv1.DeploymentSpec{ 108 | Template: corev1.PodTemplateSpec{ 109 | Spec: corev1.PodSpec{ 110 | Containers: []corev1.Container{ 111 | {Name: "kes", 112 | Env: []corev1.EnvVar{ 113 | { 114 | Name: "AWS_ACCESS_KEY_ID", 115 | ValueFrom: &corev1.EnvVarSource{ 116 | SecretKeyRef: &corev1.SecretKeySelector{ 117 | LocalObjectReference: corev1.LocalObjectReference{ 118 | Name: "aws-secret", 119 | }, 120 | Key: "access-key-id", 121 | }, 122 | }}, 123 | {Name: "AWS_SECRET_ACCESS_KEY", 124 | ValueFrom: &corev1.EnvVarSource{ 125 | SecretKeyRef: &corev1.SecretKeySelector{ 126 | LocalObjectReference: corev1.LocalObjectReference{ 127 | Name: "aws-secret", 128 | }, 129 | Key: "secret-access-key", 130 | }, 131 | }}, 132 | }}, 133 | }, 134 | }, 135 | }, 136 | }, 137 | } 138 | base := utils.NewSecretStore(false) 139 | p := api.AWSProvider{} 140 | p.Service = api.AWSServiceSecretsManager 141 | prov := api.SecretStoreProvider{} 142 | prov.AWS = &p 143 | base.Spec.Provider = &prov 144 | faker := testclient.NewSimpleClientset(&deploymentWithSecretRef) 145 | opt := apis.KesToEsoOptions{ 146 | Namespace: "kes-ns", 147 | ContainerName: "kes", 148 | DeploymentName: "kubernetes-external-secrets", 149 | ToStdout: true, 150 | TargetNamespace: "", 151 | } 152 | c := KesToEsoClient{ 153 | Client: faker, 154 | Options: &opt, 155 | } 156 | ans, err := c.InstallAWSSecrets(ctx, base) 157 | if err != nil { 158 | t.Errorf("want success got %v", err) 159 | } 160 | want_ns := "kes-ns" 161 | want_key := esmeta.SecretKeySelector{ 162 | Name: "aws-secret", 163 | Namespace: &want_ns, 164 | Key: "access-key-id", 165 | } 166 | want_secret := esmeta.SecretKeySelector{ 167 | Name: "aws-secret", 168 | Namespace: &want_ns, 169 | Key: "secret-access-key", 170 | } 171 | got_key := ans.Spec.Provider.AWS.Auth.SecretRef.AccessKeyID 172 | got_secret := ans.Spec.Provider.AWS.Auth.SecretRef.SecretAccessKey 173 | if !reflect.DeepEqual(want_key, got_key) { 174 | t.Errorf("want %v got %v", want_key, got_key) 175 | } 176 | if !reflect.DeepEqual(want_secret, got_secret) { 177 | t.Errorf("want %v got %v", want_secret, got_secret) 178 | } 179 | } 180 | 181 | func TestGCPInstall(t *testing.T) { 182 | ctx := context.TODO() 183 | deploymentWithSecretRef := appsv1.Deployment{ 184 | TypeMeta: metav1.TypeMeta{ 185 | Kind: "Deployment", 186 | APIVersion: "apps/v1", 187 | }, 188 | ObjectMeta: metav1.ObjectMeta{ 189 | Name: "kubernetes-external-secrets", 190 | Namespace: "kes-ns", 191 | }, 192 | Spec: appsv1.DeploymentSpec{ 193 | Template: corev1.PodTemplateSpec{ 194 | Spec: corev1.PodSpec{ 195 | Containers: []corev1.Container{ 196 | { 197 | Name: "kes", 198 | Env: []corev1.EnvVar{ 199 | { 200 | Name: "GOOGLE_APPLICATION_CREDENTIALS", 201 | Value: "/path/to/gcp-creds.json"}, 202 | }, 203 | VolumeMounts: []corev1.VolumeMount{ 204 | {Name: "a-name", 205 | MountPath: "/path/to", 206 | }}}, 207 | }, 208 | Volumes: []corev1.Volume{ 209 | { 210 | Name: "a-name", 211 | VolumeSource: corev1.VolumeSource{ 212 | Secret: &corev1.SecretVolumeSource{ 213 | SecretName: "gcp-secret", 214 | }, 215 | }, 216 | }, 217 | }, 218 | }, 219 | }, 220 | }, 221 | } 222 | base := utils.NewSecretStore(false) 223 | p := api.GCPSMProvider{} 224 | prov := api.SecretStoreProvider{} 225 | prov.GCPSM = &p 226 | base.Spec.Provider = &prov 227 | faker := testclient.NewSimpleClientset(&deploymentWithSecretRef) 228 | opt := apis.KesToEsoOptions{ 229 | Namespace: "kes-ns", 230 | ContainerName: "kes", 231 | DeploymentName: "kubernetes-external-secrets", 232 | ToStdout: true, 233 | TargetNamespace: "", 234 | } 235 | c := KesToEsoClient{ 236 | Client: faker, 237 | Options: &opt, 238 | } 239 | ans, err := c.InstallGCPSMSecrets(ctx, base) 240 | if err != nil { 241 | t.Errorf("want success got %v", err) 242 | } 243 | want_ns := "kes-ns" 244 | want_key := esmeta.SecretKeySelector{ 245 | Name: "gcp-secret", 246 | Namespace: &want_ns, 247 | Key: "gcp-creds.json", 248 | } 249 | got_key := ans.Spec.Provider.GCPSM.Auth.SecretRef.SecretAccessKey 250 | if !reflect.DeepEqual(want_key, got_key) { 251 | t.Errorf("want %v got %v", want_key, got_key) 252 | } 253 | } 254 | 255 | func TestAzureInstall(t *testing.T) { 256 | ctx := context.TODO() 257 | deploymentWithSecretRef := appsv1.Deployment{ 258 | TypeMeta: metav1.TypeMeta{ 259 | Kind: "Deployment", 260 | APIVersion: "apps/v1", 261 | }, 262 | ObjectMeta: metav1.ObjectMeta{ 263 | Name: "kubernetes-external-secrets", 264 | Namespace: "kes-ns", 265 | }, 266 | Spec: appsv1.DeploymentSpec{ 267 | Template: corev1.PodTemplateSpec{ 268 | Spec: corev1.PodSpec{ 269 | Containers: []corev1.Container{ 270 | {Name: "kes", 271 | Env: []corev1.EnvVar{ 272 | { 273 | Name: "AZURE_TENANT_ID", 274 | Value: "tenant", 275 | }, 276 | { 277 | Name: "AZURE_CLIENT_ID", 278 | ValueFrom: &corev1.EnvVarSource{ 279 | SecretKeyRef: &corev1.SecretKeySelector{ 280 | LocalObjectReference: corev1.LocalObjectReference{ 281 | Name: "azure-secret", 282 | }, 283 | Key: "client", 284 | }, 285 | }}, 286 | { 287 | Name: "AZURE_CLIENT_SECRET", 288 | ValueFrom: &corev1.EnvVarSource{ 289 | SecretKeyRef: &corev1.SecretKeySelector{ 290 | LocalObjectReference: corev1.LocalObjectReference{ 291 | Name: "azure-secret", 292 | }, 293 | Key: "secret", 294 | }, 295 | }}}}, 296 | }, 297 | }, 298 | }, 299 | }, 300 | } 301 | base := utils.NewSecretStore(false) 302 | p := api.AzureKVProvider{} 303 | prov := api.SecretStoreProvider{} 304 | prov.AzureKV = &p 305 | base.Spec.Provider = &prov 306 | faker := testclient.NewSimpleClientset(&deploymentWithSecretRef) 307 | opt := apis.KesToEsoOptions{ 308 | Namespace: "kes-ns", 309 | ContainerName: "kes", 310 | DeploymentName: "kubernetes-external-secrets", 311 | ToStdout: true, 312 | TargetNamespace: "", 313 | } 314 | c := KesToEsoClient{ 315 | Client: faker, 316 | Options: &opt, 317 | } 318 | ans, err := c.InstallAzureKVSecrets(ctx, base) 319 | if err != nil { 320 | t.Errorf("want success got %v", err) 321 | } 322 | want_ns := "kes-ns" 323 | want_key := esmeta.SecretKeySelector{ 324 | Name: "azure-secret", 325 | Namespace: &want_ns, 326 | Key: "client", 327 | } 328 | want_secret := esmeta.SecretKeySelector{ 329 | Name: "azure-secret", 330 | Namespace: &want_ns, 331 | Key: "secret", 332 | } 333 | got_key := *ans.Spec.Provider.AzureKV.AuthSecretRef.ClientID 334 | got_secret := *ans.Spec.Provider.AzureKV.AuthSecretRef.ClientSecret 335 | if !reflect.DeepEqual(want_key, got_key) { 336 | t.Errorf("want %v got %v - %v x %v", want_key, got_key, *want_secret.Namespace, *got_key.Namespace) 337 | } 338 | if !reflect.DeepEqual(want_secret, got_secret) { 339 | t.Errorf("want %v got %v", want_secret, got_secret) 340 | } 341 | want_ten := "tenant" 342 | if *ans.Spec.Provider.AzureKV.TenantID != want_ten { 343 | t.Errorf("want tenant got %v", *ans.Spec.Provider.AzureKV.TenantID) 344 | } 345 | } 346 | 347 | func TestIBMInstall(t *testing.T) { 348 | ctx := context.TODO() 349 | deploymentWithSecretRef := appsv1.Deployment{ 350 | TypeMeta: metav1.TypeMeta{ 351 | Kind: "Deployment", 352 | APIVersion: "apps/v1", 353 | }, 354 | ObjectMeta: metav1.ObjectMeta{ 355 | Name: "kubernetes-external-secrets", 356 | Namespace: "kes-ns", 357 | }, 358 | Spec: appsv1.DeploymentSpec{ 359 | Template: corev1.PodTemplateSpec{ 360 | Spec: corev1.PodSpec{ 361 | Containers: []corev1.Container{ 362 | {Name: "kes", 363 | Env: []corev1.EnvVar{ 364 | { 365 | Name: "IBM_CLOUD_SECRETS_MANAGER_API_ENDPOINT", 366 | Value: "tenant", 367 | }, 368 | { 369 | Name: "IBM_CLOUD_SECRETS_MANAGER_API_APIKEY", 370 | ValueFrom: &corev1.EnvVarSource{ 371 | SecretKeyRef: &corev1.SecretKeySelector{ 372 | LocalObjectReference: corev1.LocalObjectReference{ 373 | Name: "ibm-secret", 374 | }, 375 | Key: "secret", 376 | }, 377 | }}, 378 | }}, 379 | }, 380 | }, 381 | }, 382 | }, 383 | } 384 | base := utils.NewSecretStore(false) 385 | p := api.IBMProvider{} 386 | prov := api.SecretStoreProvider{} 387 | prov.IBM = &p 388 | base.Spec.Provider = &prov 389 | faker := testclient.NewSimpleClientset(&deploymentWithSecretRef) 390 | opt := apis.KesToEsoOptions{ 391 | Namespace: "kes-ns", 392 | ContainerName: "kes", 393 | DeploymentName: "kubernetes-external-secrets", 394 | ToStdout: true, 395 | TargetNamespace: "", 396 | } 397 | c := KesToEsoClient{ 398 | Client: faker, 399 | Options: &opt, 400 | } 401 | ans, err := c.InstallIBMSecrets(ctx, base) 402 | if err != nil { 403 | t.Errorf("want success got %v", err) 404 | } 405 | want_ns := "kes-ns" 406 | want_secret := esmeta.SecretKeySelector{ 407 | Name: "ibm-secret", 408 | Namespace: &want_ns, 409 | Key: "secret", 410 | } 411 | got_secret := ans.Spec.Provider.IBM.Auth.SecretRef.SecretAPIKey 412 | if !reflect.DeepEqual(want_secret, got_secret) { 413 | t.Errorf("want %v got %v", want_secret, got_secret) 414 | } 415 | want_url := "tenant" 416 | if *ans.Spec.Provider.IBM.ServiceURL != want_url { 417 | t.Errorf("want %v got %s", want_url, *ans.Spec.Provider.IBM.ServiceURL) 418 | } 419 | } 420 | 421 | func TestVaultInstall(t *testing.T) { 422 | ctx := context.TODO() 423 | want_path := "kuber-path" 424 | want_role := "kuber-role" 425 | want_url := "https://localhost" 426 | deploymentWithSecretRef := appsv1.Deployment{ 427 | TypeMeta: metav1.TypeMeta{ 428 | Kind: "Deployment", 429 | APIVersion: "apps/v1", 430 | }, 431 | ObjectMeta: metav1.ObjectMeta{ 432 | Name: "kubernetes-external-secrets", 433 | Namespace: "kes-ns", 434 | }, 435 | Spec: appsv1.DeploymentSpec{ 436 | Template: corev1.PodTemplateSpec{ 437 | Spec: corev1.PodSpec{ 438 | Containers: []corev1.Container{ 439 | {Name: "kes", 440 | Env: []corev1.EnvVar{ 441 | { 442 | Name: "VAULT_ADDR", 443 | Value: want_url, 444 | }, 445 | { 446 | Name: "DEFAULT_VAULT_MOUNT_POINT", 447 | Value: want_path, 448 | }, 449 | { 450 | Name: "DEFAULT_VAULT_ROLE", 451 | Value: want_role, 452 | }, 453 | }}, 454 | }, 455 | }, 456 | }, 457 | }, 458 | } 459 | base := utils.NewSecretStore(false) 460 | p := api.VaultProvider{} 461 | prov := api.SecretStoreProvider{} 462 | prov.Vault = &p 463 | base.Spec.Provider = &prov 464 | faker := testclient.NewSimpleClientset(&deploymentWithSecretRef) 465 | opt := apis.KesToEsoOptions{ 466 | Namespace: "kes-ns", 467 | ContainerName: "kes", 468 | DeploymentName: "kubernetes-external-secrets", 469 | ToStdout: true, 470 | TargetNamespace: "", 471 | } 472 | c := KesToEsoClient{ 473 | Client: faker, 474 | Options: &opt, 475 | } 476 | ans, err := c.InstallVaultSecrets(ctx, base) 477 | if err != nil { 478 | t.Errorf("want success got %v", err) 479 | } 480 | got_role := ans.Spec.Provider.Vault.Auth.Kubernetes.Role 481 | got_path := ans.Spec.Provider.Vault.Auth.Kubernetes.Path 482 | if !reflect.DeepEqual(want_path, got_path) { 483 | t.Errorf("want %v got %v", want_path, got_path) 484 | } 485 | if want_role != got_role { 486 | t.Errorf("want %v got %s", want_role, got_role) 487 | } 488 | got_url := ans.Spec.Provider.Vault.Server 489 | if want_url != got_url { 490 | t.Errorf("want %v got %s", want_url, got_url) 491 | } 492 | } 493 | -------------------------------------------------------------------------------- /pkg/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "kestoeso/pkg/apis" 6 | "os" 7 | 8 | api "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1" 9 | esmeta "github.com/external-secrets/external-secrets/apis/meta/v1" 10 | corev1 "k8s.io/api/core/v1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | 13 | yaml "sigs.k8s.io/yaml" 14 | ) 15 | 16 | func IsKES(K apis.KESExternalSecret) bool { 17 | return K.Kind == "ExternalSecret" && K.ApiVersion == "kubernetes-client.io/v1" 18 | } 19 | 20 | func NewSecretStore(secretStore bool) api.SecretStore { 21 | d := api.SecretStore{} 22 | d.TypeMeta = metav1.TypeMeta{ 23 | APIVersion: "external-secrets.io/v1alpha1", 24 | } 25 | if secretStore { 26 | d.TypeMeta.Kind = "SecretStore" 27 | } else { 28 | d.TypeMeta.Kind = "ClusterSecretStore" 29 | } 30 | return d 31 | } 32 | 33 | func WriteYaml(S interface{}, filepath string, to_stdout bool) error { 34 | dat, err := yaml.Marshal(S) 35 | if err != nil { 36 | return err 37 | } 38 | if to_stdout { 39 | fmt.Println(string(dat)) 40 | NewYaml() 41 | } else { 42 | err = os.WriteFile(filepath, dat, 0644) 43 | if err != nil { 44 | return err 45 | } 46 | } 47 | return nil 48 | } 49 | 50 | func UpdateOrCreateSecret(secret *corev1.Secret, essecret *esmeta.SecretKeySelector, secretValue string) (*corev1.Secret, error) { 51 | secret.Name = essecret.Name 52 | secret.Namespace = *essecret.Namespace 53 | secret.Type = corev1.SecretTypeOpaque 54 | secret.TypeMeta.Kind = "Secret" 55 | secret.TypeMeta.APIVersion = "v1" 56 | if len(secret.StringData) > 0 { 57 | secret.StringData[essecret.Key] = secretValue 58 | } else { 59 | temp := map[string]string{ 60 | essecret.Key: secretValue, 61 | } 62 | secret.StringData = temp 63 | } 64 | return secret, nil 65 | } 66 | func NewYaml() { 67 | fmt.Println("---") 68 | } 69 | -------------------------------------------------------------------------------- /rollback.sh: -------------------------------------------------------------------------------- 1 | KES_NAMESPACE="kes" 2 | ESO_NAMESPACE="es" 3 | 4 | kubectl scale deployment -n $ESO_NAMESPACE external-secrets --replicas=0 5 | kubectl scale deployment -n $KES_NAMESPACE kubernetes-external-secrets --replicas=1 6 | 7 | 8 | ## Check manually that secrets are now with owner Referenced to kubernetes-client 9 | ## then kubectl delete -f eso_files 10 | --------------------------------------------------------------------------------