├── .github ├── dependabot.yml └── workflows │ ├── ci.yaml │ └── release.yaml ├── .gitignore ├── .golangci.yaml ├── .goreleaser.yml ├── LICENSE ├── README.md ├── api └── v1alpha1 │ ├── groupversion_info.go │ ├── sopssecret_types.go │ └── zz_generated.deepcopy.go ├── build └── Dockerfile ├── config └── crd │ └── craftypath.github.io_sopssecrets.yaml ├── controllers ├── sopssecret_controller.go └── sopssecret_controller_test.go ├── go.mod ├── go.sum ├── hack └── boilerplate.go.txt ├── magefile.go ├── main.go ├── pkg ├── sops │ └── sops.go ├── tools │ └── tools.go └── version │ └── version.go ├── setup.sh └── tag.sh /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: daily 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | ci: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: checkout 14 | uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Setup Go 19 | uses: actions/setup-go@v2 20 | with: 21 | go-version: 1.17 22 | 23 | - uses: actions/cache@v2 24 | with: 25 | path: ~/go/pkg/mod 26 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 27 | restore-keys: | 28 | ${{ runner.os }}-go- 29 | 30 | - name: Install tools 31 | run: | 32 | ./setup.sh 33 | echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" 34 | 35 | - name: Check License Headers 36 | run: mage -v checkLicenseHeaders 37 | 38 | - name: Lint 39 | run: mage -v lint 40 | 41 | - name: Test 42 | run: mage -v test 43 | 44 | - name: Build 45 | run: mage -v build 46 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: Version 8 | required: true 9 | 10 | jobs: 11 | release: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: checkout 15 | uses: actions/checkout@v2 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Setup Go 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: 1.17 23 | 24 | - uses: actions/cache@v2 25 | with: 26 | path: ~/go/pkg/mod 27 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 28 | restore-keys: | 29 | ${{ runner.os }}-go- 30 | 31 | - name: Install tools 32 | run: | 33 | ./setup.sh 34 | echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" 35 | 36 | - name: Tag 37 | run: | 38 | git config user.name "$GITHUB_ACTOR" 39 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com" 40 | tag='${{ github.event.inputs.version }}' 41 | git tag --annotate --message "Tag for release $tag" "$tag" 42 | git push origin "refs/tags/$tag" 43 | 44 | - name: Test 45 | run: | 46 | mage -v test 47 | 48 | - name: Release 49 | run: | 50 | echo "$DOCKER_TOKEN" | docker login --username "$DOCKER_USERNAME" --password-stdin docker.io 51 | mage -v release 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.CI_TOKEN }} 54 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} 55 | DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }} 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/bin 2 | .idea 3 | cover.out 4 | dist 5 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 2m 3 | allow-parallel-runners: true 4 | 5 | linter-settings: 6 | misspell: 7 | locale: US 8 | 9 | linters: 10 | disable-all: true 11 | enable: 12 | - deadcode 13 | - errcheck 14 | - gofmt 15 | - goimports 16 | - gosimple 17 | - govet 18 | - ineffassign 19 | - misspell 20 | - revive 21 | - staticcheck 22 | - structcheck 23 | - typecheck 24 | - unused 25 | - varcheck 26 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod download 4 | builds: 5 | - main: main.go 6 | binary: sops-operator 7 | env: 8 | - CGO_ENABLED=0 9 | goarch: 10 | - amd64 11 | goos: 12 | - linux 13 | ldflags: 14 | - >- 15 | -X github.com/craftypath/sops-operator/pkg/version.Version={{ .Tag }} 16 | -X github.com/craftypath/sops-operator/pkg/version.GitCommit={{ .Commit }} 17 | -X github.com/craftypath/sops-operator/pkg/version.BuildDate={{ .Date }} 18 | checksum: 19 | name_template: 'checksums.txt' 20 | snapshot: 21 | name_template: "{{ .Tag }}-next" 22 | archives: 23 | - files: 24 | - LICENSE 25 | - README.md 26 | dockers: 27 | - goos: linux 28 | goarch: amd64 29 | ids: 30 | - sops-operator 31 | skip_push: false 32 | dockerfile: build/Dockerfile 33 | image_templates: 34 | - craftypath/sops-operator:{{ .Tag }} 35 | - craftypath/sops-operator:latest 36 | build_flag_templates: 37 | - --label=org.opencontainers.image.version={{ .Version }} 38 | - --label=org.opencontainers.image.revision={{ .Commit }} 39 | - --label=org.opencontainers.image.title={{ .ProjectName }} 40 | - --label=org.opencontainers.image.created={{ .Date }} 41 | - --label=org.opencontainers.image.description=The Kubernetes operator for Mozilla SOPS 42 | - --label=org.opencontainers.image.vendor=craftypath 43 | - --label=org.opencontainers.image.licenses=Apache-2.0 44 | - --label=org.opencontainers.image.source=https://github.com/craftypath/sops-operator 45 | - --label=org.opencontainers.image.authors=The SOPS Operator Authors 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | https://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SOPS Operator 2 | 3 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 4 | ![](https://github.com/craftypath/sops-operator/workflows/CI/badge.svg?branch=master) 5 | 6 | A Kubernetes operator for [Mozilla SOPS](https://github.com/mozilla/sops). 7 | 8 | ## Overview 9 | 10 | Put SOPS-encrypted data into a `SopsSecret` which can then be committed to a Git repository. 11 | Once deployed on a Kubernetes cluster, the SOPS Operator will decrypt the data and create a standard Kubernetes `Secret` from it. 12 | 13 | *Example for a SopsSecret:* 14 | 15 | ```yaml 16 | apiVersion: craftypath.github.io/v1alpha1 17 | kind: SopsSecret 18 | metadata: 19 | name: test-secret 20 | spec: 21 | metadata: 22 | labels: 23 | mylabel: mylabelvalue 24 | annotations: 25 | myannotation: myannotationvalue 26 | stringData: 27 | test.yaml: | 28 | test: ENC[AES256_GCM,data:xo8jZTsQ,iv:DTouw1kgBLok6BbR5vx8366fFavV70QeCWGNQPhNb9s=,tag:RAjeoNhvGUezdOS4YOorfA==,type:str] 29 | sops: 30 | kms: [] 31 | gcp_kms: [] 32 | azure_kv: 33 | - vault_url: https://myakskeyvault12345567.vault.azure.net 34 | name: sops 35 | version: 08faa451b1d04b8bacec0395fc8539f1 36 | created_at: '2020-05-01T19:42:49Z' 37 | enc: DvZNm3tfyoyWibQcVPts9ODRPs3aaHbRaXOPIx1Ukypa2nPmU4RCTchBPUoqscIxDjKpSy9k6A_dfE8XAu8-XrEyuOGCEy-i6Q1OtZSGW1XnWfWXPic5TF7XCVz_08h1My1RzVUr51PPNX9uazCqQeUTfBx05KC1bT3entgfttHp-98uZkZNaI8IUUnPGCH8bZzthsXRSvRQpbZcNoOW3y04pLAVYN3xVSOdDWQSElmntg_t7eVdCsmj4iXrC-J80VPU6BoZetcsQhOLjAhXHEYMOP7fqjd2bXob59Ad8rblUDwwtcZrku5lF_LVvAKGBURxockQXmEuVAjqha1SyA 38 | lastmodified: '2020-05-01T19:42:50Z' 39 | mac: ENC[AES256_GCM,data:L4YfHJ59L+/YFMTizeSmEz3QiFbNYoRBVeAJNbHOCUU0W7Iv/WfGnZuNnG5c3gOELYafc812CxCFHYwoLK0bLxOd+KHwGp5IBZ7zqrg91e04V/7Tc3iEYCE3YuTQZ56XMeSSKsct7HT7jxzmVMjW0ozJ06vzQCEC/Ljsl2NfFNs=,iv:RiBXtk6Gpc/MZvDRaGKlvA8A0K7E7bGdhs8tVa6LL5w=,tag:hwnh954tiRC/VBp6LQ6nPg==,type:str] 40 | pgp: [] 41 | unencrypted_suffix: _unencrypted 42 | version: 3.5.0 43 | ``` 44 | 45 | *Here's the Secret that's created from it:* 46 | 47 | ```yaml 48 | apiVersion: v1 49 | kind: Secret 50 | metadata: 51 | name: test-secret 52 | labels: 53 | mylabel: mylabelvalue 54 | annotations: 55 | myannotation: myannotationvalue 56 | data: 57 | test.yaml: dGVzdDogdGVzdHZhbHVlCg== 58 | ``` 59 | 60 | ## Installation 61 | 62 | A Helm chart is available in our charts repo at https://github.com/craftypath/helm-charts. 63 | 64 | ```console 65 | helm repo add craftypath https://craftypath.github.io/helm-charts 66 | helm install craftypath/sops-operator 67 | ``` 68 | 69 | Check out the chart's documentation for configuration options. 70 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The SOPS Operator Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1alpha1 contains API Schema definitions for the v1alpha1 API group 18 | //+kubebuilder:object:generate=true 19 | //+groupName=craftypath.github.io 20 | package v1alpha1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "craftypath.github.io", Version: "v1alpha1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /api/v1alpha1/sopssecret_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The SOPS Operator Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | corev1 "k8s.io/api/core/v1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 25 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 26 | 27 | // SopsSecretObjectMeta defines metadata for generated Secrets. 28 | type SopsSecretObjectMeta struct { 29 | // Annotations allows adding annotations to generated Secrets. 30 | // +optional 31 | Annotations map[string]string `json:"annotations,omitempty"` 32 | 33 | // Labels allows adding labels to generated Secrets. 34 | // +optional 35 | Labels map[string]string `json:"labels,omitempty"` 36 | } 37 | 38 | // SopsSecretSpec defines the desired state of SopsSecret. 39 | type SopsSecretSpec struct { 40 | // Metadata allows adding labels and annotations to generated Secrets. 41 | // +optional 42 | Metadata SopsSecretObjectMeta `json:"metadata,omitempty"` 43 | 44 | // StringData allows specifying Sops-encrypted secret data in string form. 45 | // +optional 46 | StringData map[string]string `json:"stringData,omitempty"` 47 | 48 | // Type specifies the type of the secret. 49 | // +optional 50 | Type corev1.SecretType `json:"type,omitempty"` 51 | } 52 | 53 | // SopsSecretStatus defines the observed state of SopsSecret. 54 | type SopsSecretStatus struct { 55 | LastUpdate metav1.Time `json:"lastUpdate,omitempty"` 56 | Reason string `json:"reason,omitempty"` 57 | Status string `json:"status,omitempty"` 58 | } 59 | 60 | //+kubebuilder:object:root=true 61 | //+kubebuilder:subresource:status 62 | 63 | // SopsSecret is the Schema for the sopssecrets API 64 | type SopsSecret struct { 65 | metav1.TypeMeta `json:",inline"` 66 | metav1.ObjectMeta `json:"metadata,omitempty"` 67 | 68 | Spec SopsSecretSpec `json:"spec,omitempty"` 69 | Status SopsSecretStatus `json:"status,omitempty"` 70 | } 71 | 72 | //+kubebuilder:object:root=true 73 | 74 | // SopsSecretList contains a list of SopsSecret 75 | type SopsSecretList struct { 76 | metav1.TypeMeta `json:",inline"` 77 | metav1.ListMeta `json:"metadata,omitempty"` 78 | Items []SopsSecret `json:"items"` 79 | } 80 | 81 | func init() { 82 | SchemeBuilder.Register(&SopsSecret{}, &SopsSecretList{}) 83 | } 84 | -------------------------------------------------------------------------------- /api/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | // +build !ignore_autogenerated 2 | 3 | /* 4 | Copyright The SOPS Operator Authors. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Code generated by controller-gen. DO NOT EDIT. 20 | 21 | package v1alpha1 22 | 23 | import ( 24 | runtime "k8s.io/apimachinery/pkg/runtime" 25 | ) 26 | 27 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 28 | func (in *SopsSecret) DeepCopyInto(out *SopsSecret) { 29 | *out = *in 30 | out.TypeMeta = in.TypeMeta 31 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 32 | out.Spec = in.Spec 33 | out.Status = in.Status 34 | } 35 | 36 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SopsSecret. 37 | func (in *SopsSecret) DeepCopy() *SopsSecret { 38 | if in == nil { 39 | return nil 40 | } 41 | out := new(SopsSecret) 42 | in.DeepCopyInto(out) 43 | return out 44 | } 45 | 46 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 47 | func (in *SopsSecret) DeepCopyObject() runtime.Object { 48 | if c := in.DeepCopy(); c != nil { 49 | return c 50 | } 51 | return nil 52 | } 53 | 54 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 55 | func (in *SopsSecretList) DeepCopyInto(out *SopsSecretList) { 56 | *out = *in 57 | out.TypeMeta = in.TypeMeta 58 | in.ListMeta.DeepCopyInto(&out.ListMeta) 59 | if in.Items != nil { 60 | in, out := &in.Items, &out.Items 61 | *out = make([]SopsSecret, len(*in)) 62 | for i := range *in { 63 | (*in)[i].DeepCopyInto(&(*out)[i]) 64 | } 65 | } 66 | } 67 | 68 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SopsSecretList. 69 | func (in *SopsSecretList) DeepCopy() *SopsSecretList { 70 | if in == nil { 71 | return nil 72 | } 73 | out := new(SopsSecretList) 74 | in.DeepCopyInto(out) 75 | return out 76 | } 77 | 78 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 79 | func (in *SopsSecretList) DeepCopyObject() runtime.Object { 80 | if c := in.DeepCopy(); c != nil { 81 | return c 82 | } 83 | return nil 84 | } 85 | 86 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 87 | func (in *SopsSecretSpec) DeepCopyInto(out *SopsSecretSpec) { 88 | *out = *in 89 | } 90 | 91 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SopsSecretSpec. 92 | func (in *SopsSecretSpec) DeepCopy() *SopsSecretSpec { 93 | if in == nil { 94 | return nil 95 | } 96 | out := new(SopsSecretSpec) 97 | in.DeepCopyInto(out) 98 | return out 99 | } 100 | 101 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 102 | func (in *SopsSecretStatus) DeepCopyInto(out *SopsSecretStatus) { 103 | *out = *in 104 | } 105 | 106 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SopsSecretStatus. 107 | func (in *SopsSecretStatus) DeepCopy() *SopsSecretStatus { 108 | if in == nil { 109 | return nil 110 | } 111 | out := new(SopsSecretStatus) 112 | in.DeepCopyInto(out) 113 | return out 114 | } 115 | -------------------------------------------------------------------------------- /build/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.access.redhat.com/ubi8/ubi-minimal 2 | 3 | RUN microdnf update -y && \ 4 | microdnf install -y shadow-utils && \ 5 | rm -rf /var/cache/yum && \ 6 | microdnf clean all 7 | 8 | RUN groupadd --gid 1000 sops-operator && \ 9 | useradd --uid 1000 --gid 1000 sops-operator 10 | 11 | RUN curl -fsSLo /usr/local/bin/sops https://github.com/mozilla/sops/releases/download/v3.7.1/sops-v3.7.1.linux && \ 12 | chmod +x /usr/local/bin/sops 13 | 14 | USER sops-operator 15 | WORKDIR /home/sops-operator 16 | 17 | COPY sops-operator /usr/local/bin/ 18 | 19 | ENTRYPOINT ["sops-operator"] 20 | -------------------------------------------------------------------------------- /config/crd/craftypath.github.io_sopssecrets.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | controller-gen.kubebuilder.io/version: v0.7.0 8 | creationTimestamp: null 9 | name: sopssecrets.craftypath.github.io 10 | spec: 11 | group: craftypath.github.io 12 | names: 13 | kind: SopsSecret 14 | listKind: SopsSecretList 15 | plural: sopssecrets 16 | singular: sopssecret 17 | scope: Namespaced 18 | versions: 19 | - name: v1alpha1 20 | schema: 21 | openAPIV3Schema: 22 | description: SopsSecret is the Schema for the sopssecrets API 23 | properties: 24 | apiVersion: 25 | description: 'APIVersion defines the versioned schema of this representation 26 | of an object. Servers should convert recognized schemas to the latest 27 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 28 | type: string 29 | kind: 30 | description: 'Kind is a string value representing the REST resource this 31 | object represents. Servers may infer this from the endpoint the client 32 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 33 | type: string 34 | metadata: 35 | type: object 36 | spec: 37 | description: SopsSecretSpec defines the desired state of SopsSecret. 38 | properties: 39 | metadata: 40 | description: Metadata allows adding labels and annotations to generated 41 | Secrets. 42 | properties: 43 | annotations: 44 | additionalProperties: 45 | type: string 46 | description: Annotations allows adding annotations to generated 47 | Secrets. 48 | type: object 49 | labels: 50 | additionalProperties: 51 | type: string 52 | description: Labels allows adding labels to generated Secrets. 53 | type: object 54 | type: object 55 | stringData: 56 | additionalProperties: 57 | type: string 58 | description: StringData allows specifying Sops-encrypted secret data 59 | in string form. 60 | type: object 61 | type: 62 | description: Type specifies the type of the secret. 63 | type: string 64 | type: object 65 | status: 66 | description: SopsSecretStatus defines the observed state of SopsSecret. 67 | properties: 68 | lastUpdate: 69 | format: date-time 70 | type: string 71 | reason: 72 | type: string 73 | status: 74 | type: string 75 | type: object 76 | type: object 77 | served: true 78 | storage: true 79 | subresources: 80 | status: {} 81 | status: 82 | acceptedNames: 83 | kind: "" 84 | plural: "" 85 | conditions: [] 86 | storedVersions: [] 87 | -------------------------------------------------------------------------------- /controllers/sopssecret_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The SOPS Operator Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "math" 23 | "time" 24 | "unicode" 25 | 26 | corev1 "k8s.io/api/core/v1" 27 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 | "k8s.io/apimachinery/pkg/runtime" 29 | "k8s.io/client-go/tools/record" 30 | ctrl "sigs.k8s.io/controller-runtime" 31 | "sigs.k8s.io/controller-runtime/pkg/client" 32 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 33 | "sigs.k8s.io/controller-runtime/pkg/log" 34 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 35 | 36 | craftypathgithubiov1alpha1 "github.com/craftypath/sops-operator/api/v1alpha1" 37 | ) 38 | 39 | type Decryptor interface { 40 | Decrypt(fileName string, encrypted string) ([]byte, error) 41 | } 42 | 43 | // SopsSecretReconciler reconciles a SopsSecret object 44 | type SopsSecretReconciler struct { 45 | client.Client 46 | Scheme *runtime.Scheme 47 | Recorder record.EventRecorder 48 | Decryptor Decryptor 49 | } 50 | 51 | //+kubebuilder:rbac:groups=craftypath.github.io,resources=sopssecrets,verbs=get;list;watch;create;update;patch;delete 52 | //+kubebuilder:rbac:groups=craftypath.github.io,resources=sopssecrets/status,verbs=get;update;patch 53 | //+kubebuilder:rbac:groups=craftypath.github.io,resources=sopssecrets/finalizers,verbs=update 54 | //+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete 55 | //+kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;create;update 56 | 57 | func (r *SopsSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 58 | reqLogger := log.FromContext(ctx) 59 | reqLogger.Info("reconciling SopsSecret") 60 | 61 | // Fetch the SopsSecret instance 62 | instance := &craftypathgithubiov1alpha1.SopsSecret{} 63 | if err := r.Get(ctx, req.NamespacedName, instance); err != nil { 64 | return reconcile.Result{}, client.IgnoreNotFound(err) 65 | } 66 | 67 | secret := &corev1.Secret{ 68 | ObjectMeta: metav1.ObjectMeta{ 69 | Name: instance.Name, 70 | Namespace: instance.Namespace, 71 | }, 72 | } 73 | 74 | result, err := ctrl.CreateOrUpdate(ctx, r.Client, secret, func() error { 75 | if !secret.CreationTimestamp.IsZero() { 76 | if !metav1.IsControlledBy(secret, instance) { 77 | return fmt.Errorf("secret already exists and not owned by sops-operator") 78 | } 79 | } 80 | if err := r.update(ctx, secret, instance); err != nil { 81 | return fmt.Errorf("failed to update secret: %w", err) 82 | } 83 | return nil 84 | }) 85 | if err != nil { 86 | return r.manageError(ctx, instance, err) 87 | } 88 | 89 | return r.manageSuccess(ctx, instance, result) 90 | } 91 | 92 | func (r *SopsSecretReconciler) update(ctx context.Context, secret *corev1.Secret, sopsSecret *craftypathgithubiov1alpha1.SopsSecret) error { 93 | logger := log.FromContext(ctx) 94 | logger.Info("handling Secret update") 95 | 96 | data := make(map[string][]byte, len(sopsSecret.Spec.StringData)) 97 | for fileName, encryptedContents := range sopsSecret.Spec.StringData { 98 | logger.Info("decrypting data", "fileName", fileName) 99 | decrypted, err := r.Decryptor.Decrypt(fileName, encryptedContents) 100 | if err != nil { 101 | return err 102 | } 103 | data[fileName] = decrypted 104 | } 105 | 106 | secret.Annotations = sopsSecret.Spec.Metadata.Annotations 107 | secret.Labels = sopsSecret.Spec.Metadata.Labels 108 | secret.Data = data 109 | if sopsSecret.Spec.Type != "" { 110 | secret.Type = sopsSecret.Spec.Type 111 | } 112 | 113 | logger.Info("setting controller reference") 114 | if err := ctrl.SetControllerReference(sopsSecret, secret, r.Scheme); err != nil { 115 | return fmt.Errorf("unable to set ownerReference: %w", err) 116 | } 117 | return nil 118 | } 119 | 120 | func (r *SopsSecretReconciler) manageError(ctx context.Context, instance *craftypathgithubiov1alpha1.SopsSecret, issue error) (reconcile.Result, error) { 121 | logger := log.FromContext(ctx) 122 | logger.Info("handling reconciliation error") 123 | 124 | r.Recorder.Event(instance, "Warning", "ProcessingError", capitalizeFirst(issue.Error())) 125 | 126 | lastUpdate := instance.Status.LastUpdate 127 | lastStatus := instance.Status.Status 128 | 129 | status := craftypathgithubiov1alpha1.SopsSecretStatus{ 130 | LastUpdate: metav1.Now(), 131 | Reason: issue.Error(), 132 | Status: "Failure", 133 | } 134 | instance.Status = status 135 | 136 | if err := r.Status().Update(ctx, instance); err != nil { 137 | logger.Error(err, "unable to update status") 138 | return reconcile.Result{ 139 | RequeueAfter: time.Second, 140 | Requeue: true, 141 | }, nil 142 | } 143 | 144 | var retryInterval time.Duration 145 | if lastUpdate.IsZero() || lastStatus == "Success" { 146 | retryInterval = time.Second 147 | } else { 148 | retryInterval = status.LastUpdate.Sub(lastUpdate.Time.Round(time.Second)) 149 | } 150 | 151 | reqeueAfter := time.Duration(math.Min(float64(retryInterval.Nanoseconds()*2), float64(time.Hour.Nanoseconds()*6))) 152 | logger.Error(issue, "failed to reconcile SopsSecret", "reqeueAfter", reqeueAfter) 153 | return reconcile.Result{ 154 | RequeueAfter: reqeueAfter, 155 | Requeue: true, 156 | }, nil 157 | } 158 | 159 | func (r *SopsSecretReconciler) manageSuccess(ctx context.Context, instance *craftypathgithubiov1alpha1.SopsSecret, result controllerutil.OperationResult) (reconcile.Result, error) { 160 | logger := log.FromContext(ctx) 161 | logger.Info("handling reconciliation success") 162 | 163 | if result == controllerutil.OperationResultNone { 164 | return reconcile.Result{}, nil 165 | } 166 | 167 | status := craftypathgithubiov1alpha1.SopsSecretStatus{ 168 | LastUpdate: metav1.Now(), 169 | Reason: "", 170 | Status: "Success", 171 | } 172 | instance.Status = status 173 | 174 | if err := r.Status().Update(ctx, instance); err != nil { 175 | logger.Error(err, "unable to update status") 176 | r.Recorder.Event(instance, "Warning", "ProcessingError", "Unable to update status") 177 | return reconcile.Result{ 178 | RequeueAfter: time.Second, 179 | Requeue: true, 180 | }, nil 181 | } 182 | 183 | opResult := capitalizeFirst(string(result)) 184 | msg := fmt.Sprintf("%s secret: %s", opResult, instance.Name) 185 | logger.Info("status updated successfully: " + msg) 186 | r.Recorder.Event(instance, "Normal", opResult, msg) 187 | return reconcile.Result{}, nil 188 | } 189 | 190 | func capitalizeFirst(s string) string { 191 | if len(s) == 0 { 192 | return "" 193 | } 194 | tmp := []rune(s) 195 | tmp[0] = unicode.ToUpper(tmp[0]) 196 | return string(tmp) 197 | } 198 | 199 | // SetupWithManager sets up the controller with the Manager. 200 | func (r *SopsSecretReconciler) SetupWithManager(mgr ctrl.Manager) error { 201 | return ctrl.NewControllerManagedBy(mgr). 202 | For(&craftypathgithubiov1alpha1.SopsSecret{}). 203 | Owns(&corev1.Secret{}). 204 | Complete(r) 205 | } 206 | -------------------------------------------------------------------------------- /controllers/sopssecret_controller_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The SOPS Operator Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | "os" 22 | "testing" 23 | 24 | "go.uber.org/zap/zapcore" 25 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 26 | 27 | "github.com/craftypath/sops-operator/api/v1alpha1" 28 | "github.com/stretchr/testify/assert" 29 | "github.com/stretchr/testify/require" 30 | uberzap "go.uber.org/zap" 31 | corev1 "k8s.io/api/core/v1" 32 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 | "k8s.io/apimachinery/pkg/runtime" 34 | "k8s.io/apimachinery/pkg/types" 35 | "k8s.io/client-go/kubernetes/scheme" 36 | "k8s.io/client-go/tools/record" 37 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 38 | logf "sigs.k8s.io/controller-runtime/pkg/log" 39 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 40 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 41 | ) 42 | 43 | type FakeDecryptor struct{} 44 | 45 | func (f *FakeDecryptor) Decrypt(fileName string, encrypted string) ([]byte, error) { 46 | return []byte("unencrypted"), nil 47 | } 48 | 49 | func TestMain(m *testing.M) { 50 | logf.SetLogger( 51 | zap.New(zap.UseDevMode(true), 52 | zap.Encoder(zapcore.NewConsoleEncoder(uberzap.NewDevelopmentEncoderConfig()))), 53 | ) 54 | 55 | os.Exit(m.Run()) 56 | } 57 | 58 | var ( 59 | name = "test-secret" 60 | namespace = "test-namespace" 61 | req = reconcile.Request{ 62 | NamespacedName: types.NamespacedName{ 63 | Name: name, 64 | Namespace: namespace, 65 | }, 66 | } 67 | ) 68 | 69 | func TestReconcile_Create(t *testing.T) { 70 | tests := []struct { 71 | name string 72 | sopsSecret *v1alpha1.SopsSecret 73 | }{ 74 | { 75 | name: "without metadata", 76 | sopsSecret: &v1alpha1.SopsSecret{ 77 | ObjectMeta: metav1.ObjectMeta{ 78 | Name: name, 79 | Namespace: namespace, 80 | }, 81 | Spec: v1alpha1.SopsSecretSpec{ 82 | StringData: map[string]string{"test.yaml": "encrypted"}, 83 | }, 84 | }, 85 | }, 86 | { 87 | name: "with metadata", 88 | sopsSecret: &v1alpha1.SopsSecret{ 89 | ObjectMeta: metav1.ObjectMeta{ 90 | Name: name, 91 | Namespace: namespace, 92 | }, 93 | Spec: v1alpha1.SopsSecretSpec{ 94 | Metadata: v1alpha1.SopsSecretObjectMeta{ 95 | Labels: map[string]string{"mylabel": "foo"}, 96 | Annotations: map[string]string{"myannotation": "bar"}, 97 | }, 98 | StringData: map[string]string{"test.yaml": "encrypted"}, 99 | }, 100 | }, 101 | }, 102 | } 103 | for _, tt := range tests { 104 | t.Run(tt.name, func(t *testing.T) { 105 | s := runtime.NewScheme() 106 | utilruntime.Must(scheme.AddToScheme(s)) 107 | utilruntime.Must(v1alpha1.AddToScheme(s)) 108 | 109 | recorder := record.NewFakeRecorder(1) 110 | r := newSopsSecretReconciler(s, recorder, tt.sopsSecret) 111 | 112 | res, err := r.Reconcile(context.Background(), req) 113 | require.NoError(t, err) 114 | assert.False(t, res.Requeue) 115 | 116 | secret := &corev1.Secret{} 117 | err = r.Get(context.Background(), req.NamespacedName, secret) 118 | require.NoError(t, err) 119 | assert.Equal(t, []byte("unencrypted"), secret.Data["test.yaml"]) 120 | assert.Equal(t, tt.sopsSecret.Spec.Metadata.Labels, secret.Labels) 121 | assert.Equal(t, tt.sopsSecret.Spec.Metadata.Annotations, secret.Annotations) 122 | event := <-recorder.Events 123 | assert.Equal(t, event, "Normal Created Created secret: test-secret") 124 | }) 125 | } 126 | } 127 | 128 | func TestReconcile_Update(t *testing.T) { 129 | sopsSecret := &v1alpha1.SopsSecret{ 130 | ObjectMeta: metav1.ObjectMeta{ 131 | Name: name, 132 | Namespace: namespace, 133 | }, 134 | } 135 | 136 | s := runtime.NewScheme() 137 | utilruntime.Must(scheme.AddToScheme(s)) 138 | utilruntime.Must(v1alpha1.AddToScheme(s)) 139 | 140 | recorder := record.NewFakeRecorder(2) 141 | r := newSopsSecretReconciler(s, recorder, sopsSecret) 142 | 143 | res, err := r.Reconcile(context.Background(), req) 144 | require.NoError(t, err) 145 | assert.False(t, res.Requeue) 146 | event := <-recorder.Events 147 | assert.Equal(t, event, "Normal Created Created secret: test-secret") 148 | 149 | secret := &corev1.Secret{} 150 | err = r.Get(context.Background(), req.NamespacedName, secret) 151 | require.NoError(t, err) 152 | assert.Empty(t, secret.Labels) 153 | assert.Empty(t, secret.Annotations) 154 | 155 | err = r.Get(context.Background(), req.NamespacedName, sopsSecret) 156 | require.NoError(t, err) 157 | 158 | sopsSecret.Spec.Metadata.Labels = map[string]string{ 159 | "mylabel": "foo", 160 | } 161 | sopsSecret.Spec.Metadata.Annotations = map[string]string{ 162 | "myannotation": "bar", 163 | } 164 | 165 | err = r.Update(context.Background(), sopsSecret) 166 | require.NoError(t, err) 167 | 168 | res, err = r.Reconcile(context.Background(), req) 169 | require.NoError(t, err) 170 | assert.False(t, res.Requeue) 171 | err = r.Get(context.Background(), req.NamespacedName, secret) 172 | require.NoError(t, err) 173 | assert.Equal(t, sopsSecret.Spec.Metadata.Labels, secret.Labels) 174 | assert.Equal(t, sopsSecret.Spec.Metadata.Annotations, secret.Annotations) 175 | event = <-recorder.Events 176 | assert.Equal(t, event, "Normal Updated Updated secret: test-secret") 177 | } 178 | 179 | func TestExistingSecretNotOwnedByUs(t *testing.T) { 180 | secret := &corev1.Secret{ 181 | ObjectMeta: metav1.ObjectMeta{ 182 | Name: name, 183 | Namespace: namespace, 184 | CreationTimestamp: metav1.Now(), 185 | }, 186 | } 187 | 188 | sopsSecret := &v1alpha1.SopsSecret{ 189 | ObjectMeta: metav1.ObjectMeta{ 190 | Name: name, 191 | Namespace: namespace, 192 | }, 193 | } 194 | 195 | s := runtime.NewScheme() 196 | utilruntime.Must(scheme.AddToScheme(s)) 197 | utilruntime.Must(v1alpha1.AddToScheme(s)) 198 | 199 | recorder := record.NewFakeRecorder(1) 200 | r := newSopsSecretReconciler(s, recorder, secret, sopsSecret) 201 | 202 | _, err := r.Reconcile(context.Background(), req) 203 | require.NoError(t, err) 204 | event := <-recorder.Events 205 | assert.Contains(t, event, "Secret already exists and not owned by sops-operator") 206 | 207 | err = r.Delete(context.Background(), secret) 208 | require.NoError(t, err) 209 | 210 | _, err = r.Reconcile(context.Background(), req) 211 | require.NoError(t, err) 212 | event = <-recorder.Events 213 | assert.Contains(t, event, "Normal Created Created secret: test-secret") 214 | } 215 | 216 | func newSopsSecretReconciler(s *runtime.Scheme, recorder *record.FakeRecorder, objs ...runtime.Object) *SopsSecretReconciler { 217 | cl := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build() 218 | return &SopsSecretReconciler{ 219 | Client: cl, 220 | Scheme: s, 221 | Recorder: recorder, 222 | Decryptor: &FakeDecryptor{}, 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/craftypath/sops-operator 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/golangci/golangci-lint v1.42.1 7 | github.com/goreleaser/goreleaser v0.184.0 8 | github.com/magefile/mage v1.11.0 9 | github.com/stretchr/testify v1.7.0 10 | github.com/sykesm/zap-logfmt v0.0.4 11 | go.uber.org/zap v1.19.1 12 | golang.org/x/tools v0.1.7 13 | k8s.io/api v0.22.3 14 | k8s.io/apimachinery v0.22.3 15 | k8s.io/client-go v0.22.3 16 | sigs.k8s.io/controller-runtime v0.10.2 17 | sigs.k8s.io/controller-tools v0.7.0 18 | ) 19 | 20 | require ( 21 | 4d63.com/gochecknoglobals v0.0.0-20201008074935-acfc0b28355a // indirect 22 | cloud.google.com/go v0.94.0 // indirect 23 | cloud.google.com/go/kms v0.1.0 // indirect 24 | cloud.google.com/go/storage v1.16.1 // indirect 25 | code.gitea.io/sdk/gitea v0.15.0 // indirect 26 | github.com/AlekSi/pointer v1.1.0 // indirect 27 | github.com/Azure/azure-pipeline-go v0.2.3 // indirect 28 | github.com/Azure/azure-sdk-for-go v57.0.0+incompatible // indirect 29 | github.com/Azure/azure-storage-blob-go v0.14.0 // indirect 30 | github.com/Azure/go-autorest v14.2.0+incompatible // indirect 31 | github.com/Azure/go-autorest/autorest v0.11.20 // indirect 32 | github.com/Azure/go-autorest/autorest/adal v0.9.15 // indirect 33 | github.com/Azure/go-autorest/autorest/azure/auth v0.5.8 // indirect 34 | github.com/Azure/go-autorest/autorest/azure/cli v0.4.3 // indirect 35 | github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect 36 | github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect 37 | github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect 38 | github.com/Azure/go-autorest/logger v0.2.1 // indirect 39 | github.com/Azure/go-autorest/tracing v0.6.0 // indirect 40 | github.com/BurntSushi/toml v0.4.1 // indirect 41 | github.com/DisgoOrg/disgohook v1.4.3 // indirect 42 | github.com/DisgoOrg/log v1.1.0 // indirect 43 | github.com/DisgoOrg/restclient v1.2.7 // indirect 44 | github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect 45 | github.com/Masterminds/goutils v1.1.1 // indirect 46 | github.com/Masterminds/semver v1.5.0 // indirect 47 | github.com/Masterminds/semver/v3 v3.1.1 // indirect 48 | github.com/Masterminds/sprig v2.22.0+incompatible // indirect 49 | github.com/Microsoft/go-winio v0.4.16 // indirect 50 | github.com/OpenPeeDeeP/depguard v1.0.1 // indirect 51 | github.com/ProtonMail/go-crypto v0.0.0-20210512092938-c05353c2d58c // indirect 52 | github.com/alecthomas/jsonschema v0.0.0-20211022214203-8b29eab41725 // indirect 53 | github.com/alexkohler/prealloc v1.0.0 // indirect 54 | github.com/apex/log v1.9.0 // indirect 55 | github.com/ashanbrown/forbidigo v1.2.0 // indirect 56 | github.com/ashanbrown/makezero v0.0.0-20210520155254-b6261585ddde // indirect 57 | github.com/atc0005/go-teams-notify/v2 v2.6.0 // indirect 58 | github.com/aws/aws-sdk-go v1.40.34 // indirect 59 | github.com/aws/aws-sdk-go-v2 v1.9.0 // indirect 60 | github.com/aws/aws-sdk-go-v2/config v1.7.0 // indirect 61 | github.com/aws/aws-sdk-go-v2/credentials v1.4.0 // indirect 62 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.5.0 // indirect 63 | github.com/aws/aws-sdk-go-v2/internal/ini v1.2.2 // indirect 64 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.0 // indirect 65 | github.com/aws/aws-sdk-go-v2/service/kms v1.5.0 // indirect 66 | github.com/aws/aws-sdk-go-v2/service/sso v1.4.0 // indirect 67 | github.com/aws/aws-sdk-go-v2/service/sts v1.7.0 // indirect 68 | github.com/aws/smithy-go v1.8.0 // indirect 69 | github.com/beorn7/perks v1.0.1 // indirect 70 | github.com/bkielbasa/cyclop v1.2.0 // indirect 71 | github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect 72 | github.com/bombsimon/wsl/v3 v3.3.0 // indirect 73 | github.com/caarlos0/ctrlc v1.0.0 // indirect 74 | github.com/caarlos0/env/v6 v6.7.0 // indirect 75 | github.com/caarlos0/go-shellwords v1.0.12 // indirect 76 | github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e // indirect 77 | github.com/cenkalti/backoff v2.1.1+incompatible // indirect 78 | github.com/cespare/xxhash/v2 v2.1.1 // indirect 79 | github.com/charithe/durationcheck v0.0.8 // indirect 80 | github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af // indirect 81 | github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect 82 | github.com/daixiang0/gci v0.2.9 // indirect 83 | github.com/davecgh/go-spew v1.1.1 // indirect 84 | github.com/denis-tingajkin/go-header v0.4.2 // indirect 85 | github.com/dghubble/go-twitter v0.0.0-20210609183100-2fdbf421508e // indirect 86 | github.com/dghubble/oauth1 v0.7.0 // indirect 87 | github.com/dghubble/sling v1.3.0 // indirect 88 | github.com/dimchansky/utfbom v1.1.1 // indirect 89 | github.com/emirpasic/gods v1.12.0 // indirect 90 | github.com/esimonov/ifshort v1.0.2 // indirect 91 | github.com/ettle/strcase v0.1.1 // indirect 92 | github.com/evanphx/json-patch v4.11.0+incompatible // indirect 93 | github.com/fatih/color v1.12.0 // indirect 94 | github.com/fatih/structtag v1.2.0 // indirect 95 | github.com/fsnotify/fsnotify v1.5.1 // indirect 96 | github.com/fzipp/gocyclo v0.3.1 // indirect 97 | github.com/go-critic/go-critic v0.5.6 // indirect 98 | github.com/go-git/gcfg v1.5.0 // indirect 99 | github.com/go-git/go-billy/v5 v5.1.0 // indirect 100 | github.com/go-git/go-git/v5 v5.3.0 // indirect 101 | github.com/go-logr/logr v0.4.0 // indirect 102 | github.com/go-logr/zapr v0.4.0 // indirect 103 | github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect 104 | github.com/go-toolsmith/astcast v1.0.0 // indirect 105 | github.com/go-toolsmith/astcopy v1.0.0 // indirect 106 | github.com/go-toolsmith/astequal v1.0.0 // indirect 107 | github.com/go-toolsmith/astfmt v1.0.0 // indirect 108 | github.com/go-toolsmith/astp v1.0.0 // indirect 109 | github.com/go-toolsmith/strparse v1.0.0 // indirect 110 | github.com/go-toolsmith/typep v1.0.2 // indirect 111 | github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b // indirect 112 | github.com/gobuffalo/flect v0.2.3 // indirect 113 | github.com/gobwas/glob v0.2.3 // indirect 114 | github.com/gofrs/flock v0.8.1 // indirect 115 | github.com/gogo/protobuf v1.3.2 // indirect 116 | github.com/golang-jwt/jwt/v4 v4.0.0 // indirect 117 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 118 | github.com/golang/protobuf v1.5.2 // indirect 119 | github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 // indirect 120 | github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect 121 | github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 // indirect 122 | github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a // indirect 123 | github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 // indirect 124 | github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca // indirect 125 | github.com/golangci/misspell v0.3.5 // indirect 126 | github.com/golangci/revgrep v0.0.0-20210208091834-cd28932614b5 // indirect 127 | github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect 128 | github.com/google/go-cmp v0.5.6 // indirect 129 | github.com/google/go-github/v39 v39.2.0 // indirect 130 | github.com/google/go-querystring v1.1.0 // indirect 131 | github.com/google/gofuzz v1.1.0 // indirect 132 | github.com/google/rpmpack v0.0.0-20210410105602-e20c988a6f5a // indirect 133 | github.com/google/uuid v1.3.0 // indirect 134 | github.com/google/wire v0.5.0 // indirect 135 | github.com/googleapis/gax-go/v2 v2.1.0 // indirect 136 | github.com/googleapis/gnostic v0.5.5 // indirect 137 | github.com/gordonklaus/ineffassign v0.0.0-20210225214923-2e10b2664254 // indirect 138 | github.com/goreleaser/chglog v0.1.2 // indirect 139 | github.com/goreleaser/fileglob v1.2.0 // indirect 140 | github.com/goreleaser/nfpm/v2 v2.6.0 // indirect 141 | github.com/gorilla/websocket v1.4.2 // indirect 142 | github.com/gostaticanalysis/analysisutil v0.4.1 // indirect 143 | github.com/gostaticanalysis/comment v1.4.1 // indirect 144 | github.com/gostaticanalysis/forcetypeassert v0.0.0-20200621232751-01d4955beaa5 // indirect 145 | github.com/gostaticanalysis/nilerr v0.1.1 // indirect 146 | github.com/hashicorp/errwrap v1.0.0 // indirect 147 | github.com/hashicorp/go-cleanhttp v0.5.1 // indirect 148 | github.com/hashicorp/go-multierror v1.1.1 // indirect 149 | github.com/hashicorp/go-retryablehttp v0.6.8 // indirect 150 | github.com/hashicorp/go-version v1.2.1 // indirect 151 | github.com/hashicorp/hcl v1.0.0 // indirect 152 | github.com/huandu/xstrings v1.3.2 // indirect 153 | github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 // indirect 154 | github.com/imdario/mergo v0.3.12 // indirect 155 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 156 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 157 | github.com/jgautheron/goconst v1.5.1 // indirect 158 | github.com/jingyugao/rowserrcheck v1.1.0 // indirect 159 | github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect 160 | github.com/jmespath/go-jmespath v0.4.0 // indirect 161 | github.com/json-iterator/go v1.1.11 // indirect 162 | github.com/julz/importas v0.0.0-20210419104244-841f0c0fe66d // indirect 163 | github.com/kevinburke/ssh_config v1.1.0 // indirect 164 | github.com/kisielk/errcheck v1.6.0 // indirect 165 | github.com/kisielk/gotool v1.0.0 // indirect 166 | github.com/kulti/thelper v0.4.0 // indirect 167 | github.com/kunwardeep/paralleltest v1.0.2 // indirect 168 | github.com/kyoh86/exportloopref v0.1.8 // indirect 169 | github.com/ldez/gomoddirectives v0.2.2 // indirect 170 | github.com/ldez/tagliatelle v0.2.0 // indirect 171 | github.com/magiconair/properties v1.8.5 // indirect 172 | github.com/maratori/testpackage v1.0.1 // indirect 173 | github.com/matoous/godox v0.0.0-20210227103229-6504466cf951 // indirect 174 | github.com/mattn/go-colorable v0.1.8 // indirect 175 | github.com/mattn/go-ieproxy v0.0.1 // indirect 176 | github.com/mattn/go-isatty v0.0.12 // indirect 177 | github.com/mattn/go-runewidth v0.0.9 // indirect 178 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect 179 | github.com/mbilski/exhaustivestruct v1.2.0 // indirect 180 | github.com/mgechev/dots v0.0.0-20190921121421-c36f7dcfbb81 // indirect 181 | github.com/mgechev/revive v1.1.1 // indirect 182 | github.com/mitchellh/copystructure v1.1.2 // indirect 183 | github.com/mitchellh/go-homedir v1.1.0 // indirect 184 | github.com/mitchellh/mapstructure v1.4.1 // indirect 185 | github.com/mitchellh/reflectwalk v1.0.1 // indirect 186 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 187 | github.com/modern-go/reflect2 v1.0.1 // indirect 188 | github.com/moricho/tparallel v0.2.1 // indirect 189 | github.com/nakabonne/nestif v0.3.0 // indirect 190 | github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 // indirect 191 | github.com/nishanths/exhaustive v0.2.3 // indirect 192 | github.com/nishanths/predeclared v0.2.1 // indirect 193 | github.com/olekukonko/tablewriter v0.0.5 // indirect 194 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect 195 | github.com/pelletier/go-toml v1.9.3 // indirect 196 | github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d // indirect 197 | github.com/pkg/errors v0.9.1 // indirect 198 | github.com/pmezard/go-difflib v1.0.0 // indirect 199 | github.com/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349 // indirect 200 | github.com/prometheus/client_golang v1.11.1 // indirect 201 | github.com/prometheus/client_model v0.2.0 // indirect 202 | github.com/prometheus/common v0.26.0 // indirect 203 | github.com/prometheus/procfs v0.6.0 // indirect 204 | github.com/quasilyte/go-ruleguard v0.3.4 // indirect 205 | github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95 // indirect 206 | github.com/russross/blackfriday/v2 v2.0.1 // indirect 207 | github.com/ryancurrah/gomodguard v1.2.3 // indirect 208 | github.com/ryanrolds/sqlclosecheck v0.3.0 // indirect 209 | github.com/sanposhiho/wastedassign/v2 v2.0.6 // indirect 210 | github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b // indirect 211 | github.com/securego/gosec/v2 v2.8.1 // indirect 212 | github.com/sergi/go-diff v1.2.0 // indirect 213 | github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect 214 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect 215 | github.com/sirupsen/logrus v1.8.1 // indirect 216 | github.com/slack-go/slack v0.9.4 // indirect 217 | github.com/sonatard/noctx v0.0.1 // indirect 218 | github.com/sourcegraph/go-diff v0.6.1 // indirect 219 | github.com/spf13/afero v1.6.0 // indirect 220 | github.com/spf13/cast v1.3.1 // indirect 221 | github.com/spf13/cobra v1.2.1 // indirect 222 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 223 | github.com/spf13/pflag v1.0.5 // indirect 224 | github.com/spf13/viper v1.8.1 // indirect 225 | github.com/ssgreg/nlreturn/v2 v2.1.0 // indirect 226 | github.com/stretchr/objx v0.2.0 // indirect 227 | github.com/subosito/gotenv v1.2.0 // indirect 228 | github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b // indirect 229 | github.com/technoweenie/multipartstreamer v1.0.1 // indirect 230 | github.com/tetafro/godot v1.4.9 // indirect 231 | github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94 // indirect 232 | github.com/tomarrell/wrapcheck/v2 v2.3.0 // indirect 233 | github.com/tommy-muehle/go-mnd/v2 v2.4.0 // indirect 234 | github.com/ulikunitz/xz v0.5.10 // indirect 235 | github.com/ultraware/funlen v0.0.3 // indirect 236 | github.com/ultraware/whitespace v0.0.4 // indirect 237 | github.com/uudashr/gocognit v1.0.5 // indirect 238 | github.com/vartanbeno/go-reddit/v2 v2.0.0 // indirect 239 | github.com/xanzy/go-gitlab v0.50.3 // indirect 240 | github.com/xanzy/ssh-agent v0.3.0 // indirect 241 | github.com/yeya24/promlinter v0.1.0 // indirect 242 | go.opencensus.io v0.23.0 // indirect 243 | go.uber.org/atomic v1.9.0 // indirect 244 | go.uber.org/multierr v1.7.0 // indirect 245 | gocloud.dev v0.24.0 // indirect 246 | golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b // indirect 247 | golang.org/x/mod v0.5.0 // indirect 248 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect 249 | golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect 250 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect 251 | golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e // indirect 252 | golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect 253 | golang.org/x/text v0.3.7 // indirect 254 | golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect 255 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 256 | gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect 257 | google.golang.org/api v0.56.0 // indirect 258 | google.golang.org/appengine v1.6.7 // indirect 259 | google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2 // indirect 260 | google.golang.org/grpc v1.40.0 // indirect 261 | google.golang.org/protobuf v1.27.1 // indirect 262 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect 263 | gopkg.in/inf.v0 v0.9.1 // indirect 264 | gopkg.in/ini.v1 v1.62.0 // indirect 265 | gopkg.in/mail.v2 v2.3.1 // indirect 266 | gopkg.in/warnings.v0 v0.1.2 // indirect 267 | gopkg.in/yaml.v2 v2.4.0 // indirect 268 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 269 | honnef.co/go/tools v0.2.1 // indirect 270 | k8s.io/apiextensions-apiserver v0.22.2 // indirect 271 | k8s.io/component-base v0.22.2 // indirect 272 | k8s.io/klog/v2 v2.9.0 // indirect 273 | k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e // indirect 274 | k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a // indirect 275 | mvdan.cc/gofumpt v0.1.1 // indirect 276 | mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect 277 | mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect 278 | mvdan.cc/unparam v0.0.0-20210104141923-aac4ce9116a7 // indirect 279 | sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect 280 | sigs.k8s.io/yaml v1.2.0 // indirect 281 | ) 282 | 283 | require github.com/Antonboom/errname v0.1.4 // indirect 284 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The SOPS Operator Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | -------------------------------------------------------------------------------- /magefile.go: -------------------------------------------------------------------------------- 1 | //go:build mage 2 | // +build mage 3 | 4 | /* 5 | Copyright The SOPS Operator Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | https://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | "bufio" 24 | "errors" 25 | "fmt" 26 | "io/fs" 27 | "os" 28 | "path/filepath" 29 | "strings" 30 | 31 | "github.com/magefile/mage/mg" 32 | "github.com/magefile/mage/sh" 33 | ) 34 | 35 | func Lint() error { 36 | if err := sh.RunV("bash", "-c", "shopt -s globstar; shellcheck **/*.sh"); err != nil { 37 | return err 38 | } 39 | if err := sh.RunV("golangci-lint", "run"); err != nil { 40 | return err 41 | } 42 | if err := sh.RunV("go", "mod", "tidy"); err != nil { 43 | return err 44 | } 45 | return sh.RunV("git", "diff", "--exit-code") 46 | } 47 | 48 | func Format() error { 49 | if err := sh.RunV("gofmt", "-s", "-w", "."); err != nil { 50 | return err 51 | } 52 | if err := sh.RunV("goimports", "-w", "."); err != nil { 53 | return err 54 | } 55 | return nil 56 | } 57 | 58 | func CheckLicenseHeaders() error { 59 | var checkFailed bool 60 | 61 | if err := filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error { 62 | if err != nil { 63 | return err 64 | } 65 | if d.IsDir() { 66 | return nil 67 | } 68 | ext := filepath.Ext(path) 69 | if ext == ".sh" || ext == ".go" { 70 | fmt.Print("Checking ", path, " ") 71 | 72 | f, err := os.Open(path) 73 | if err != nil { 74 | return err 75 | } 76 | defer f.Close() 77 | 78 | var hasCopyright bool 79 | var hasLicense bool 80 | 81 | scanner := bufio.NewScanner(f) 82 | // only check first 20 lines 83 | for i := 0; i < 20 && scanner.Scan(); i++ { 84 | line := scanner.Text() 85 | if !hasCopyright && strings.Contains(line, "Copyright The SOPS Operator Authors") { 86 | hasCopyright = true 87 | } 88 | if !hasLicense && strings.Contains(line, "https://www.apache.org/licenses/LICENSE-2.0") { 89 | hasLicense = true 90 | } 91 | } 92 | 93 | if !(hasCopyright && hasLicense) { 94 | fmt.Println("❌") 95 | checkFailed = true 96 | } else { 97 | fmt.Println("☑️") 98 | } 99 | 100 | return nil 101 | } 102 | return nil 103 | }); err != nil { 104 | return err 105 | } 106 | 107 | if checkFailed { 108 | return errors.New("file(s) without license header found") 109 | } 110 | return nil 111 | } 112 | 113 | func ControllerGen() error { 114 | if err := sh.RunV("controller-gen", "crd", "paths=./...", "output:crd:artifacts:config=config/crd"); err != nil { 115 | return err 116 | } 117 | if err := sh.RunV("controller-gen", "crd", "object:headerFile=hack/boilerplate.go.txt"); err != nil { 118 | return err 119 | } 120 | return nil 121 | } 122 | 123 | func Test() error { 124 | return sh.RunV("go", "test", "./...", "-race") 125 | } 126 | 127 | func Build() error { 128 | return sh.RunV("goreleaser", "release", "--rm-dist", "--snapshot") 129 | } 130 | 131 | func Release() error { 132 | mg.Deps(Test) 133 | return sh.RunV("goreleaser", "release", "--rm-dist") 134 | } 135 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The SOPS Operator Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "fmt" 22 | "os" 23 | "strings" 24 | "time" 25 | 26 | "github.com/craftypath/sops-operator/pkg/sops" 27 | zaplogfmt "github.com/sykesm/zap-logfmt" 28 | uzap "go.uber.org/zap" 29 | "go.uber.org/zap/zapcore" 30 | "k8s.io/apimachinery/pkg/runtime" 31 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 32 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 33 | 34 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 35 | // to ensure that exec-entrypoint and run can make use of them. 36 | _ "k8s.io/client-go/plugin/pkg/client/auth" 37 | ctrl "sigs.k8s.io/controller-runtime" 38 | "sigs.k8s.io/controller-runtime/pkg/cache" 39 | "sigs.k8s.io/controller-runtime/pkg/healthz" 40 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 41 | 42 | "github.com/craftypath/sops-operator/api/v1alpha1" 43 | "github.com/craftypath/sops-operator/controllers" 44 | //+kubebuilder:scaffold:imports 45 | ) 46 | 47 | var ( 48 | scheme = runtime.NewScheme() 49 | setupLog = ctrl.Log.WithName("setup") 50 | ) 51 | 52 | const controllerName string = "sopssecret-controller" 53 | 54 | // watchNamespaceEnvVar is the constant for env variable WATCH_NAMESPACE 55 | // which specifies the namespaces (comma-separated) to watch. 56 | // An empty value means the operator is running with cluster scope. 57 | const watchNamespaceEnvVar = "WATCH_NAMESPACE" 58 | 59 | func init() { 60 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 61 | utilruntime.Must(v1alpha1.AddToScheme(scheme)) 62 | //+kubebuilder:scaffold:scheme 63 | } 64 | 65 | func main() { 66 | var metricsAddr string 67 | var enableLeaderElection bool 68 | var probeAddr string 69 | flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") 70 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 71 | flag.BoolVar(&enableLeaderElection, "leader-elect", false, 72 | "Enable leader election for controller manager. "+ 73 | "Enabling this will ensure there is only one active controller manager.") 74 | 75 | logConfig := uzap.NewProductionEncoderConfig() 76 | logConfig.EncodeTime = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) { 77 | encoder.AppendString(ts.UTC().Format(time.RFC3339Nano)) 78 | } 79 | logfmtEncoder := zaplogfmt.NewEncoder(logConfig) 80 | 81 | opts := zap.Options{ 82 | Encoder: logfmtEncoder, 83 | } 84 | opts.BindFlags(flag.CommandLine) 85 | flag.Parse() 86 | 87 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 88 | 89 | watchNamespace, err := getWatchNamespace() 90 | if err != nil { 91 | setupLog.Error(err, "unable to get WatchNamespace, "+ 92 | "the manager will watch and manage resources in all Namespaces") 93 | } 94 | 95 | options := ctrl.Options{ 96 | Namespace: watchNamespace, 97 | Scheme: scheme, 98 | MetricsBindAddress: metricsAddr, 99 | Port: 9443, 100 | HealthProbeBindAddress: probeAddr, 101 | LeaderElection: enableLeaderElection, 102 | LeaderElectionID: "sops-operator-lock", 103 | } 104 | 105 | // Add support for MultiNamespace set in WATCH_NAMESPACE (e.g ns1,ns2) 106 | if strings.Contains(watchNamespace, ",") { 107 | setupLog.Info("manager set up with multiple namespaces", "namespaces", watchNamespace) 108 | // configure cluster-scoped with MultiNamespacedCacheBuilder 109 | options.Namespace = "" 110 | options.NewCache = cache.MultiNamespacedCacheBuilder(strings.Split(watchNamespace, ",")) 111 | } 112 | 113 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), options) 114 | if err != nil { 115 | setupLog.Error(err, "unable to start manager") 116 | os.Exit(1) 117 | } 118 | 119 | if err = (&controllers.SopsSecretReconciler{ 120 | Client: mgr.GetClient(), 121 | Scheme: mgr.GetScheme(), 122 | Recorder: mgr.GetEventRecorderFor(controllerName), 123 | Decryptor: &sops.Decryptor{}, 124 | }).SetupWithManager(mgr); err != nil { 125 | setupLog.Error(err, "unable to create controller", "controller", "SopsSecret") 126 | os.Exit(1) 127 | } 128 | //+kubebuilder:scaffold:builder 129 | 130 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 131 | setupLog.Error(err, "unable to set up ready check") 132 | os.Exit(1) 133 | } 134 | 135 | setupLog.Info("starting manager") 136 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 137 | setupLog.Error(err, "problem running manager") 138 | os.Exit(1) 139 | } 140 | } 141 | 142 | // getWatchNamespace returns the Namespace the operator should be watching for changes 143 | func getWatchNamespace() (string, error) { 144 | ns, found := os.LookupEnv(watchNamespaceEnvVar) 145 | if !found { 146 | return "", fmt.Errorf("%s must be set", watchNamespaceEnvVar) 147 | } 148 | return ns, nil 149 | } 150 | -------------------------------------------------------------------------------- /pkg/sops/sops.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The SOPS Operator Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package sops 18 | 19 | import ( 20 | "bytes" 21 | "fmt" 22 | "os/exec" 23 | "path/filepath" 24 | 25 | logf "sigs.k8s.io/controller-runtime/pkg/log" 26 | ) 27 | 28 | type Decryptor struct{} 29 | 30 | var ( 31 | log = logf.Log.WithName("sops") 32 | 33 | fileFormats = map[string]string{ 34 | ".yaml": "yaml", 35 | ".yml": "yaml", 36 | ".json": "json", 37 | ".ini": "ini", 38 | ".env": "dotenv", 39 | } 40 | ) 41 | 42 | // Decrypt decrypts the given encrypted string. The format (yaml, json, dotenv, init, binary) 43 | // is determined by the given fileName. 44 | func (d *Decryptor) Decrypt(fileName string, encrypted string) ([]byte, error) { 45 | format := determineFileFormat(fileName) 46 | args := []string{"--decrypt", "--input-type", format, "--output-type", format, "/dev/stdin"} 47 | log.V(1).Info("running sops", "args", args) 48 | 49 | // We shell out to SOPS because that way we get better error messages 50 | command := exec.Command("sops", args...) 51 | command.Stdin = bytes.NewBufferString(encrypted) 52 | 53 | output, err := command.Output() 54 | if err != nil { 55 | if e, ok := err.(*exec.ExitError); ok { 56 | return nil, fmt.Errorf("failed to decrypt file: %s", string(e.Stderr)) 57 | } 58 | return nil, err 59 | } 60 | return output, err 61 | } 62 | 63 | func determineFileFormat(fileName string) string { 64 | ext := filepath.Ext(fileName) 65 | if format, exists := fileFormats[ext]; exists { 66 | return format 67 | } 68 | return "binary" 69 | } 70 | -------------------------------------------------------------------------------- /pkg/tools/tools.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | /* 4 | Copyright The SOPS Operator Authors. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package tools 20 | 21 | import ( 22 | _ "github.com/golangci/golangci-lint/cmd/golangci-lint" 23 | _ "github.com/goreleaser/goreleaser" 24 | _ "github.com/magefile/mage" 25 | _ "golang.org/x/tools/cmd/goimports" 26 | _ "sigs.k8s.io/controller-tools/cmd/controller-gen" 27 | ) 28 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The SOPS Operator Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package version 18 | 19 | var ( 20 | Version = "dev" 21 | GitCommit = "HEAD" 22 | BuildDate = "unknown" 23 | ) 24 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright The SOPS Operator Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | go install github.com/golangci/golangci-lint/cmd/golangci-lint 18 | go install github.com/goreleaser/goreleaser 19 | go install github.com/magefile/mage 20 | go install golang.org/x/tools/cmd/goimports 21 | go install sigs.k8s.io/controller-tools/cmd/controller-gen 22 | -------------------------------------------------------------------------------- /tag.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright The SOPS Operator Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | SCRIPT_DIR=$(dirname "$(readlink -f "$0")") 22 | readonly SCRIPT_DIR 23 | 24 | show_help() { 25 | cat << EOF 26 | Usage: $(basename "$0") 27 | 28 | Create and push a tag. 29 | 30 | -h, --help Display help 31 | -d, --debug Display verbose output 32 | -r, --remote The name of the remote to push the tag to (default: upstream) 33 | -f, --force Force an existing tag to be overwritten 34 | -t, --tag The name of the tag to create 35 | -s, --skip-push Skip pushing the tag 36 | EOF 37 | } 38 | 39 | main() { 40 | local debug= 41 | local tag= 42 | local remote=upstream 43 | local force=() 44 | local skip_push= 45 | 46 | while :; do 47 | case "${1:-}" in 48 | -h|--help) 49 | show_help 50 | exit 51 | ;; 52 | -d|--debug) 53 | debug=true 54 | ;; 55 | -t|--tag) 56 | if [[ -n "${2:-}" ]]; then 57 | tag="$2" 58 | shift 59 | else 60 | echo "ERROR: '--tag' cannot be empty." >&2 61 | show_help 62 | exit 1 63 | fi 64 | ;; 65 | -r|--remote) 66 | if [[ -n "${2:-}" ]]; then 67 | remote="$2" 68 | shift 69 | else 70 | echo "ERROR: '--remote' cannot be empty." >&2 71 | show_help 72 | exit 1 73 | fi 74 | ;; 75 | -f|--force) 76 | force+=(--force) 77 | ;; 78 | -s|--skip-push) 79 | skip_push=true 80 | ;; 81 | *) 82 | break 83 | ;; 84 | esac 85 | 86 | shift 87 | done 88 | 89 | if [[ -z "$tag" ]]; then 90 | echo "ERROR: --tag is required!" >&2 91 | show_help 92 | exit 1 93 | fi 94 | 95 | if [[ -n "$debug" ]]; then 96 | set -x 97 | fi 98 | 99 | pushd "$SCRIPT_DIR" > /dev/null 100 | 101 | git tag -a -m "Release $tag" "$tag" "${force[@]}" 102 | 103 | if [[ -z "$skip_push" ]]; then 104 | git push "$remote" "refs/tags/$tag" "${force[@]}" 105 | fi 106 | 107 | popd > /dev/null 108 | } 109 | 110 | main "$@" 111 | --------------------------------------------------------------------------------