├── .dockerignore ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── SECURITY.md ├── api └── v1alpha1 │ ├── dnsconnector_types.go │ ├── dnsrecord_types.go │ ├── dnszone_types.go │ ├── groupversion_info.go │ └── zz_generated.deepcopy.go ├── cmd └── main.go ├── config ├── crd │ ├── bases │ │ ├── monkale.monkale.io_dnsconnectors.yaml │ │ ├── monkale.monkale.io_dnsrecords.yaml │ │ └── monkale.monkale.io_dnszones.yaml │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── patches │ │ ├── cainjection_in_dnsconnectors.yaml │ │ ├── cainjection_in_dnsrecords.yaml │ │ ├── cainjection_in_dnszones.yaml │ │ ├── webhook_in_dnsconnectors.yaml │ │ ├── webhook_in_dnsrecords.yaml │ │ └── webhook_in_dnszones.yaml ├── default │ ├── kustomization.yaml │ ├── manager_auth_proxy_patch.yaml │ └── manager_config_patch.yaml ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── manifests │ ├── bases │ │ └── coredns-manager-operator.clusterserviceversion.yaml │ └── kustomization.yaml ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── rbac │ ├── auth_proxy_client_clusterrole.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_role_binding.yaml │ ├── auth_proxy_service.yaml │ ├── dnsconnector_editor_role.yaml │ ├── dnsconnector_viewer_role.yaml │ ├── dnsrecord_editor_role.yaml │ ├── dnsrecord_viewer_role.yaml │ ├── dnszone_editor_role.yaml │ ├── dnszone_viewer_role.yaml │ ├── kustomization.yaml │ ├── leader_election_role.yaml │ ├── leader_election_role_binding.yaml │ ├── role.yaml │ ├── role_binding.yaml │ └── service_account.yaml ├── samples │ ├── kustomization.yaml │ ├── monkale_v1alpha1_dnsconnector.yaml │ ├── monkale_v1alpha1_dnsrecord.yaml │ └── monkale_v1alpha1_dnszone.yaml └── scorecard │ ├── bases │ └── config.yaml │ ├── kustomization.yaml │ └── patches │ ├── basic.config.yaml │ └── olm.config.yaml ├── deploy ├── .gitkeep └── operator.yaml ├── docs ├── coredns_exposure.md ├── develop.md ├── dnsconnector.md ├── dnsrecords.md ├── dnszones.md ├── qa │ ├── dev-manual-qa-guide.md │ └── pics │ │ ├── describe-coredns-mounts-1.png │ │ └── describe-corefile-1.png └── troubleshoot.md ├── go.mod ├── go.sum ├── hack └── boilerplate.go.txt └── internal └── controller ├── dnsconnector_construction.go ├── dnsconnector_controller.go ├── dnsrecord_construction.go ├── dnsrecord_controller.go ├── dnszone_construction.go ├── dnszone_controller.go ├── finalizers.go ├── obj_lookup.go └── suite_test.go /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Bug Report 11 | 12 | ## Description 13 | 14 | Provide a clear and concise description of the bug. 15 | 16 | ## Steps to Reproduce 17 | 18 | Please provide detailed steps to reproduce the issue: 19 | 20 | 1. Execute '...' 21 | 2. Apply '...' 22 | 3. See error 23 | 24 | ## Expected Behavior 25 | 26 | Describe what you expected to happen. 27 | 28 | ## Actual Behavior 29 | 30 | Describe what actually happened. Include any error messages or screenshots. 31 | 32 | ## Environment 33 | 34 | Provide details about the environment in which the issue occurred: 35 | 36 | - **CoreDNS Manager Operator version**: [e.g., 1.0.0] 37 | - **CoreDNS version**: [e.g., 1.10.1] 38 | - **Kubernetes version**: [e.g., 1.21.0] 39 | - **Kubernetes distro**: [e.g., k3s, rke2, talos] 40 | - **Operating System**: [e.g., Ubuntu 20.04] 41 | 42 | ## Additional Context 43 | 44 | Add any other context about the problem here. 45 | 46 | --- 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin/* 9 | Dockerfile.cross 10 | 11 | # Test binary, build with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Kubernetes Generated files - skip generated files, except for vendored files 18 | 19 | !vendor/**/zz_generated.* 20 | 21 | # editor and IDE paraphernalia 22 | .idea 23 | .vscode 24 | *.swp 25 | *.swo 26 | *~ 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [1.0.3] - 2024-06-13 11 | ### Fixed 12 | - 13 | - Bug Fix: Resolve validation of the spec.primaryNS.hostname allowing the creation of more complex hostnames required for certain DNS setups. 14 | https://github.com/monkale-io/coredns-manager-operator/issues/7 15 | 16 | ## [1.0.2] - 2024-06-05 17 | ### Fixed 18 | - Bug Fix: Resolve Zone File Mount Issues with Dashed Domain Names 19 | - Fixed a bug where domain names containing dashes were causing issues with zone file mounting. 20 | 21 | ## [1.0.1] - 2024-06-04 22 | ### Added 23 | - Initial release with all functionalities. 24 | - Basic functionality for managing DNS records. 25 | - Integration with CoreDNS. 26 | - Support for Kubernetes custom resources `dnsrecord` and `dnszone`. 27 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | monkaleio@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. 3 | 4 | Please note we have a code of conduct, please follow it in all your interactions with the project. 5 | 6 | ## Table of Contents 7 | 8 | 1. [Code of Conduct](#code-of-conduct) 9 | 2. [How to Contribute](#how-to-contribute) 10 | - [Reporting Bugs](#reporting-bugs) 11 | - [Suggesting Enhancements](#suggesting-enhancements) 12 | - [Submitting Pull Requests](#submitting-pull-requests) 13 | 3. [Development Setup](#development-setup) 14 | 4. [Style Guides](#style-guides) 15 | - [Git Commit Messages](#git-commit-messages) 16 | - [Go Code Style](#go-code-style) 17 | 18 | ## Code of Conduct 19 | 20 | Please read and follow our [Code of Conduct](CODE_OF_CONDUCT.md) to keep our community approachable and respectful. 21 | 22 | ## How to Contribute 23 | 24 | ### Reporting Bugs 25 | 26 | If you find a bug in the project, please check if the bug is already reported. If not, open a new issue and include the following details: 27 | - A clear and descriptive title. 28 | - A detailed description of the bug. 29 | - Steps to reproduce the bug. 30 | - Any relevant logs, error messages, or screenshots. 31 | 32 | ### Suggesting Enhancements 33 | 34 | If you have an idea for an enhancement or a new feature, please open an issue and describe: 35 | - The problem you’re trying to solve. 36 | - The solution or enhancement you propose. 37 | - Any alternatives you’ve considered. 38 | 39 | 40 | ### Submitting Pull Requests 41 | 42 | The `develop` branch is used for ongoing development and should always be in sync with the `main` branch. All new features and bug fixes should be developed in feature branches created from `develop`, and pull requests should be submitted to the `develop` branch. 43 | 44 | 1. Fork the repository and create your branch from `develop`. 45 | 2. Ensure your code follows the project's coding standards. 46 | 3. If you’ve added or changed functionality, update the documentation accordingly. 47 | 4. Review and follow the [manual QA guide](docs/qa/dev-manual-qa-guide.md) to ensure all known use cases are checked before committing. 48 | 5. Commit your changes with a descriptive commit message. 49 | 6. Push your branch to your fork and open a Pull Request to the `develop` branch of the repository. 50 | 51 | Please ensure your pull request adheres to the following guidelines: 52 | - Provide a clear description of what your pull request does. 53 | - Include the issue number if your pull request addresses a specific issue. 54 | - Follow the [manual QA guide](docs/qa/dev-manual-qa-guide.md) 55 | - Review the changes to make sure they are well-tested and documented. 56 | 57 | The repository owner will: 58 | 1. Review and test the changes. 59 | 2. Merge the changes into the `main` branch after successful review and testing. 60 | 3. Build the image and push it to Docker Hub. 61 | 4. Update CHANGELOG.md 62 | 5. Set up the tag. 63 | 64 | ## Development Setup 65 | 66 | To set up the project for development, please refer to the [development guide](docs/develop.md). 67 | 68 | ## Style Guides 69 | 70 | ### Git Commit Messages 71 | 72 | - Use the present tense ("Add feature" not "Added feature"). 73 | - Use the imperative mood ("Move cursor to..." not "Moves cursor to..."). 74 | - Limit the first line to 72 characters or less. 75 | - Reference issues and pull requests liberally after the first line. 76 | 77 | ### Go Code Style 78 | 79 | - Write clear, concise comments for exported functions and types. 80 | - Use descriptive names for variables and functions. 81 | 82 | Thank you for your contributions! 83 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.20 as builder 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | 6 | WORKDIR /workspace 7 | # Copy the Go Modules manifests 8 | COPY go.mod go.mod 9 | COPY go.sum go.sum 10 | # cache deps before building and copying source so that we don't need to re-download as much 11 | # and so that source changes don't invalidate our downloaded layer 12 | RUN go mod download 13 | 14 | # Copy the go source 15 | COPY cmd/main.go cmd/main.go 16 | COPY api/ api/ 17 | COPY internal/controller/ internal/controller/ 18 | 19 | # Build 20 | # the GOARCH has not a default value to allow the binary be built according to the host where the command 21 | # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO 22 | # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, 23 | # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. 24 | RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go 25 | 26 | # Use distroless as minimal base image to package the manager binary 27 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 28 | FROM gcr.io/distroless/static:nonroot 29 | WORKDIR / 30 | COPY --from=builder /workspace/manager . 31 | USER 65532:65532 32 | 33 | ENTRYPOINT ["/manager"] 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | # Code generated by tool. DO NOT EDIT. 2 | # This file is used to track the info used to scaffold your project 3 | # and allow the plugins properly work. 4 | # More info: https://book.kubebuilder.io/reference/project-config.html 5 | domain: monkale.io 6 | layout: 7 | - go.kubebuilder.io/v4 8 | plugins: 9 | manifests.sdk.operatorframework.io/v2: {} 10 | scorecard.sdk.operatorframework.io/v2: {} 11 | projectName: coredns-manager-operator 12 | repo: github.com/monkale.io/coredns-manager-operator 13 | resources: 14 | - api: 15 | crdVersion: v1 16 | namespaced: true 17 | controller: true 18 | domain: monkale.io 19 | group: monkale 20 | kind: DNSZone 21 | path: github.com/monkale.io/coredns-manager-operator/api/v1alpha1 22 | version: v1alpha1 23 | - api: 24 | crdVersion: v1 25 | namespaced: true 26 | controller: true 27 | domain: monkale.io 28 | group: monkale 29 | kind: DNSRecord 30 | path: github.com/monkale.io/coredns-manager-operator/api/v1alpha1 31 | version: v1alpha1 32 | - api: 33 | crdVersion: v1 34 | namespaced: true 35 | controller: true 36 | domain: monkale.io 37 | group: monkale 38 | kind: DNSConnector 39 | path: github.com/monkale.io/coredns-manager-operator/api/v1alpha1 40 | version: v1alpha1 41 | version: "3" 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **This project is no longer maintained. Open for adoption!** 2 | 3 | # Coredns-manager-operator 4 | The CoreDNS Manager Operator is designed for offline or home lab setups, eliminating the need for additional DNS software like dnsmasq or named and leveraging a GitOps approach for managing DNS configurations. 5 | 6 | In many on-premises environments, managing DNS records can be complex and often requires separate DNS servers. With the CoreDNS Manager Operator, you can handle internal DNS directly within your Kubernetes cluster, simplifying the process and reducing infrastructure needs. This operator integrates with Kubernetes, making it easy to manage DNS records efficiently using a GitOps workflow. 7 | 8 | ## Features 9 | * Manage DNS records within a Kubernetes cluster using `DNSRecord` and `DNSZone` CRDs. 10 | * Attach DNS zones to Kubernetes' CoreDNS server. 11 | * Automatically generate zone files, including SOA and NS records. 12 | * Support all DNS record types specified by RFC1035. 13 | * Good choice for home labs and disconnected environments, eliminating the need for additional DNS software and enables GitOps approach for managing DNS configurations. 14 | 15 | ## Limitations 16 | * Compatibility: Currently tested with k3s(v1.29.5+k3s1), talos(kubernetes v1.30.1) and KinD(0.20.0). It is expected to work with other Kubernetes distributions. 17 | 18 | * CoreDNS Exposure: The project is designed to manage CoreDNS zones, but it does not expose CoreDNS to the network by default. Administrators must handle exposure themselves using methods such as LoadBalancer services, iptables, NodePorts, HAProxy, IngressRoutes or even kubectl port-forwarding. Visit this guide ([docs/coredns_exposure.md](docs/coredns_exposure.md)) to see common methods. 19 | 20 | ## Project Resources 21 | * DNSRecord: Represents individual DNS records such as A, AAAA, CNAME, etc. 22 | 23 | [DNSRecord Documentation](docs/dnsrecords.md) 24 | 25 | * DNSZone: Defines the domain (SOA) and groups all related DNSRecord resources into a zone file. 26 | 27 | [DNSZones Documentation](docs/dnszones.md) 28 | 29 | * DNSConnector: Integrates the operator with Kubernetes' CoreDNS. It ensures that any changes to DNSRecord and DNSZone resources are reflected in CoreDNS. 30 | 31 | [DNSConnector Documentation](docs/dnsconnector.md) 32 | 33 | ## Quick start 34 | During this guide you we will briefly learn coredns-manager-operator' resources and debug commands. In case of problems visit [troubleshoot guide](docs/troubleshoot.md). 35 | 36 | 37 | **> IMPORTANT:** Make sure to deploy the coredns-manager-operator and its resources in the same namespace as CoreDNS, usually `kube-system`. Always include the `--namespace kube-system` to all your queries or just switch the context. 38 | 39 | 1. Expose CoreDNS to your network: Ensure CoreDNS is accessible on port 53 (TCP/UDP). The method depends on your deployment. Visit this guide ([docs/coredns_exposure.md](docs/coredns_exposure.md)) to see common methods. 40 | 41 | ```sh 42 | # in this example 192.168.122.10 is kubernetes cluster. 43 | # udp 44 | $ nc -zvuw3 192.168.122.10 53 45 | Connection to 192.168.122.10 53 port [udp/domain] succeeded! 46 | # tcp test 47 | $ nc -zvw3 192.168.122.10 53 48 | Connection to 192.168.122.10 53 port [tcp/domain] succeeded! 49 | ``` 50 | 51 | 2. Increase Coredns replicas(Optional) 52 | 53 | Zonefile updates can cause a short interruption in name resolution, typically lasting from <1 to 5 seconds. To minimize downtime during updates, consider increasing CoreDNS replicas to 2 or more. 54 | 55 | 3. Install the operator. By default it will install in `kube-system` namespace. Modify the manifest if needed. 56 | ```sh 57 | $ kubectl apply -f https://raw.githubusercontent.com/monkale-io/coredns-manager-operator/main/deploy/operator.yaml 58 | ``` 59 | 60 | Ensure that the operator is up and running 61 | ```sh 62 | $ kubectl get deployments.apps coredns-manager-operator-controller-manager -n kube-system 63 | NAME READY UP-TO-DATE AVAILABLE AGE 64 | coredns-manager-operator-controller-manager 1/1 1 1 4m10s 65 | ``` 66 | 67 | 4. Create DNSConnector: 68 | * The corednsCM should point to the CoreDNS ConfigMap, usually named `coredns` and located in the `kube-system` namespace. 69 | * The `corefileKey` points to the Corefile inside the ConfigMap, typically called `Corefile`. 70 | * The corednsDeployment refers to the CoreDNS deployment, which is generally named `coredns`. This could also be a DaemonSet or StatefulSet. 71 | 72 | In most popular Kubernetes distributions, these settings will remain the same, so you can use the following template without modifications: 73 | ```sh 74 | $ cat << EOF | kubectl apply -f - 75 | apiVersion: monkale.monkale.io/v1alpha1 76 | kind: DNSConnector 77 | metadata: 78 | name: coredns 79 | namespace: kube-system 80 | spec: 81 | corednsCM: 82 | name: coredns 83 | corefileKey: Corefile 84 | corednsDeployment: 85 | name: coredns 86 | type: Deployment 87 | EOF 88 | ``` 89 | 90 | Then check if the resource DNSConnector became "Active" 91 | ```sh 92 | $ kubectl get dnsconnectors -n kube-system 93 | NAME LAST CHANGE STATE MESSAGE 94 | coredns 2024-06-03T18:51:44Z Active CoreDNS Ready 95 | ``` 96 | 97 | 5. Create `DNSZone` 98 | Define the root of your domain. 99 | 100 | * Set your domain and set `connectorName` to point to the previously created DNSConnector resource - `coredns`. 101 | * The `ipAddress` should reflect the IP address of your Kubernetes node or Load Balancer — the address where you want CoreDNS to listen externally. 102 | * The `respPersonEmail` field is for the responsible person's (admin's) email. 103 | 104 | Use the following template: 105 | ```sh 106 | $ cat << EOF | kubectl apply -f - 107 | apiVersion: monkale.monkale.io/v1alpha1 108 | kind: DNSZone 109 | metadata: 110 | name: demo-example-zone 111 | namespace: kube-system 112 | spec: 113 | connectorName: coredns 114 | domain: "demo.example.com" 115 | primaryNS: 116 | ipAddress: "192.168.122.10" 117 | respPersonEmail: "admin@example.com" 118 | EOF 119 | ``` 120 | 121 | Check DNSZone status. It has to be "Active". 122 | ```sh 123 | $ kubectl get dnszones -n kube-system 124 | NAME DOMAIN NAME RECORD COUNT LAST CHANGE CURRENT SERIAL STATE 125 | demo-example-zone demo.example.com 0 2024-06-03T18:57:59Z 0603185759 Active 126 | ``` 127 | 128 | 6. Create your first `DNSRecord`. 129 | 130 | Let's create our first A record to point any hosts under `*.ingress.demo.example.com` to `10.100.100.10`. This will match all hosts such as `app1.ingress.demo.example.com`. 131 | 132 | * `Record.name` specifies the name of the `DNSRecord` 133 | * `Record.value` in this case specifies the IP address of the A record 134 | * `dnszoneref.name` references the `DNSZone` name created previously 135 | 136 | Create the first record: 137 | ```sh 138 | $ cat << EOF | kubectl apply -f - 139 | --- 140 | apiVersion: monkale.monkale.io/v1alpha1 141 | kind: DNSRecord 142 | metadata: 143 | name: demo-a-record 144 | namespace: kube-system 145 | spec: 146 | record: 147 | name: "*.ingress" 148 | value: "10.100.100.10" 149 | type: "A" 150 | dnsZoneRef: 151 | name: "demo-example-zone" 152 | EOF 153 | ``` 154 | 155 | Inspect the record 156 | ```sh 157 | $ kubectl get dnsrecords -n kube-system 158 | NAME RECORD NAME RECORD TYPE RECORD VALUE ZONE REFERENCE LAST CHANGE STATE 159 | demo-a-record *.ingress A 10.100.100.10 demo-example-zone 2024-06-03T19:10:18Z Ready 160 | ``` 161 | 162 | Additional info: 163 | * The following record types are supported: A, AAAA, CNAME, MX, TXT, NS, PTR, SRV, CAA, DNSKEY, DS, NAPTR, RRSIG, DNAME, HINFO. Visit `config/samples/monkale_v1alpha1_dnsrecord.yaml` to see examples. 164 | * DNSRecords are templated into the zone file as standardized by RFC 1035. Pay attention to the domain names you insert into the zone files. If a record has a dot at the end, the DNS server will treat it as a fully qualified domain name (FQDN). If there is no dot, the DNS server will add the zone file origin name to the record. 165 | 166 | More here 167 | [Troubleshoot Guide - FQDNs vs Relative names](docs/dnsrecords.md#pay-attention-to-domain-names-and-fqdns) 168 | 169 | 170 | 7. Try to resolve 171 | ```sh 172 | # try app1.ingress.market.example.com 173 | $ dig +short @192.168.122.10 app1.ingress.demo.example.com 174 | 10.100.100.10 175 | # try app2.ingress.market.example.com 176 | $ dig +short @192.168.122.10 app2.ingress.demo.example.com 177 | 10.100.100.10 178 | ``` 179 | 180 | ## Troubleshoot 181 | 182 | [Troubleshoot guide](docs/troubleshoot.md) 183 | 184 | ## Articles 185 | 186 | * [Medium: Managing Internal DNS in Air-Gapped k3s Clusters with Monkale CoreDNS-Manager-Operator](https://medium.com/@nicholas5421/managing-internal-dns-in-air-gapped-k3s-clusters-with-monkale-coredns-manager-operator-fa1c9136cc2c) 187 | * [Medium: Installing Monkale CoreDNS Manager Operator on Single-Node Talos](https://medium.com/@nicholas5421/installing-monkale-coredns-manager-operator-on-single-node-talos-16f8be900585) 188 | 189 | ## Contact 190 | 191 | * [Email - monkaleio@gmail.com](mailto:monkaleio@gmail.com) 192 | 193 | ## Contributing 194 | 195 | Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to this project. 196 | 197 | ## Changelog 198 | 199 | All notable changes to this project will be documented in [CHANGELOG.md](CHANGELOG.md). 200 | 201 | ## License 202 | 203 | Copyright 2024 monkale.io. 204 | 205 | Licensed under the Apache License, Version 2.0 (the "License"); 206 | you may not use this file except in compliance with the License. 207 | You may obtain a copy of the License at 208 | 209 | http://www.apache.org/licenses/LICENSE-2.0 210 | 211 | Unless required by applicable law or agreed to in writing, software 212 | distributed under the License is distributed on an "AS IS" BASIS, 213 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 214 | See the License for the specific language governing permissions and 215 | limitations under the License. 216 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We currently support the following version of CoreDNS Manager Operator with security updates: 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 1.0.3 | :white_check_mark: | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | If you discover a security vulnerability, please let me know by creating an issue or contacting me directly. I appreciate any feedback and will try to address issues as best as I can. 14 | 15 | **Email**: Send an email to [monkaleio@gmail.com](mailto:monkaleio@gmail.com) with the following details: 16 | - A description of the vulnerability. 17 | - Steps to reproduce the issue. 18 | - Any potential impact the vulnerability could have. 19 | -------------------------------------------------------------------------------- /api/v1alpha1/dnsconnector_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 monkale.io. 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 | http://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 | "fmt" 21 | 22 | appsv1 "k8s.io/api/apps/v1" 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | "sigs.k8s.io/controller-runtime/pkg/client" 25 | ) 26 | 27 | const ( 28 | CorednsOriginalConfBkpSuffix string = "-original-configmap" // CorednsOriginalConfBkpSuffix suffix that will be used to create a copy of the original coredns conf 29 | ConditionConnectorTypeReady string = "Ready" // ConditionConnectorTypeReady is used to update condition type 30 | ConditionReasonConnectorActive string = "Active" // ConditionReasonConnectorActive represents state of the DNSConnector 31 | ConditionReasonConnectorError string = "Error" // ConditionReasonConnectorError represents the error state of the DNSConnector 32 | ConditionReasonConnectorUpdating string = "Updating" // ConditionReasonConnectorUpdating represents the 33 | ConditionReasonConnectorUpdateErr string = "UpdateError" // ConditionReasonConnectorUpdateErr represents state of the DNSConnector 34 | ConditionReasonConnectorUnknown string = "Unknown" // ConditionReasonConnectorUnknown string = "Unknown" 35 | DnsConnectorsFinalizerName string = "dnsconnectors/finalizers" // DnsConnectorsFinalizerName is finalizer used by DNSConnector controller 36 | ) 37 | 38 | type CoreDNSConfigMap struct { 39 | // name is the name of the CoreDNS ConfigMap that contains Corefile. 40 | // The default value is "coredns" 41 | // +kubebuilder:default:=coredns 42 | // +kubebuilder:validation:Optional 43 | 44 | Name string `json:"name"` 45 | // corefileKey specifies the key whose value is the Corefile. Typically, this key is "Corefile". 46 | // The default value is "Corefile" 47 | // +kubebuilder:default:=Corefile 48 | // +kubebuilder:validation:Optional 49 | CorefileKey string `json:"corefileKey"` 50 | } 51 | 52 | // CoreDNSDeploymentType defines the desired type and name of the CoreDNS resource 53 | type CoreDNSDeploymentType struct { 54 | // type of the CoreDNS resource (e.g., Deployment, StatefulSet, DaemonSet, ReplicaSet, Pod) 55 | // +kubebuilder:validation:Enum=Deployment;StatefulSet;DaemonSet 56 | Type string `json:"type"` 57 | 58 | // name specifies the name of the CoreDNS resource. 59 | // This field is optional if Type is Pod and a LabelSelector is specified. 60 | Name string `json:"name,omitempty"` 61 | 62 | // zonefilesMountDir specifies the mountPath for zonefiles. 63 | // Default value is /opt/coredns. 64 | // +kubebuilder:default:=/opt/coredns 65 | // +kubebuilder:validation:Pattern=`^(/[^/]+)+$` 66 | // +kubebuilder:validation:Optional 67 | ZoneFileMountDir string `json:"zonefilesMountDir"` 68 | } 69 | 70 | // DNSConnectorSpec defines the desired state of DNSConnector 71 | type DNSConnectorSpec struct { 72 | // waitForUpdateTimeout specifies how long the DNSConnector for coredns to complete update. 73 | // if coredns deployment haven't complete the update, the controller will perform rollback. 74 | // The default value is 120 seconds (2 min) 75 | // +kubebuilder:default:=120 76 | WaitForUpdateTimeout int `json:"waitForUpdateTimeout"` 77 | 78 | // corednsCM is the name of the CoreDNS ConfigMap. 79 | CorednsCM CoreDNSConfigMap `json:"corednsCM"` 80 | 81 | // corednsDeployment specifies the CoreDNS deployment type and name or labels 82 | CorednsDeployment CoreDNSDeploymentType `json:"corednsDeployment"` 83 | 84 | // corednsZoneEnaledPlugins is list of enabled coredns plugins. 85 | // https://coredns.io/plugins. The most useful plugins are: 86 | // errors - prints errors to stdout; log - prints queries to stdout. 87 | // +kubebuilder:validation:Optional 88 | CorednsZoneEnaledPlugins []string `json:"corednsZoneEnaledPlugins"` 89 | } 90 | 91 | // ProvisionedDNSZone used to display the status of the zones provisioned to the Coredns 92 | type ProvisionedDNSZone struct { 93 | Name string `json:"name"` 94 | Domain string `json:"domain"` 95 | SerialNumber string `json:"serialNumber"` 96 | } 97 | 98 | // DNSConnectorStatus defines the observed state of DNSConnector 99 | type DNSConnectorStatus struct { 100 | // conditions indidicate the status of a DNSZone. 101 | // +listType=map 102 | // +listMapKey=type 103 | // +optional 104 | Conditions []metav1.Condition `json:"conditions,omitempty"` 105 | 106 | // provisionedZones maps domain names to their serial numbers. 107 | // +optional 108 | ProvisionedDNSZones []ProvisionedDNSZone `json:"provisionedZones,omitempty"` 109 | } 110 | 111 | //+kubebuilder:object:root=true 112 | //+kubebuilder:subresource:status 113 | //+kubebuilder:printcolumn:name="Last Change",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].lastTransitionTime",description="Last Change" 114 | //+kubebuilder:printcolumn:name="State",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].reason",description="The current state" 115 | //+kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="The current state" 116 | 117 | // DNSConnector is the Schema for the dnsconnectors API 118 | type DNSConnector struct { 119 | metav1.TypeMeta `json:",inline"` 120 | metav1.ObjectMeta `json:"metadata,omitempty"` 121 | 122 | Spec DNSConnectorSpec `json:"spec,omitempty"` 123 | Status DNSConnectorStatus `json:"status,omitempty"` 124 | } 125 | 126 | //+kubebuilder:object:root=true 127 | 128 | // DNSConnectorList contains a list of DNSConnector 129 | type DNSConnectorList struct { 130 | metav1.TypeMeta `json:",inline"` 131 | metav1.ListMeta `json:"metadata,omitempty"` 132 | Items []DNSConnector `json:"items"` 133 | } 134 | 135 | // AssertCorednsDeploymentType used to validate and set the resource type of Coredns deployment. Return client.Object. 136 | func AssertCorednsDeploymentType(resourceType string) (client.Object, error) { 137 | var resource client.Object 138 | switch resourceType { 139 | case "Deployment": 140 | resource = &appsv1.Deployment{} 141 | case "StatefulSet": 142 | resource = &appsv1.StatefulSet{} 143 | case "DaemonSet": 144 | resource = &appsv1.DaemonSet{} 145 | default: 146 | return nil, fmt.Errorf("unsupported resource type: %s", resourceType) 147 | } 148 | return resource, nil 149 | } 150 | 151 | func init() { 152 | SchemeBuilder.Register(&DNSConnector{}, &DNSConnectorList{}) 153 | } 154 | -------------------------------------------------------------------------------- /api/v1alpha1/dnsrecord_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 monkale.io. 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 | http://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 | "strings" 21 | 22 | corev1 "k8s.io/api/core/v1" 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | ) 25 | 26 | const ( 27 | ConditionRecordTypeReady string = "Ready" // ConditionRecordTypeReady is used to update condition type 28 | ConditionReasonRecordReady string = "Ready" // ConditionReasonRecordReady represents state of the DNSRecord 29 | ConditionReasonRecordPending string = "Pending" // ConditionReasonRecordPending represents state of the DNSRecord 30 | ConditionReasonRecordDegraded string = "Degraded" // ConditionReasonRecordDegraded represents state of the DNSRecord 31 | ConditionReasonRecordUnknown string = "Unknown" // ConditionReasonRecordUnknown represents state of the DNSRecord 32 | DnsRecorsFinalizerName string = "dnsrecords/finalizers" // DnsRecorsFinalizerName is finalizer used by DNSRecord controller 33 | DnsRecordIndex string = ".spec.dnsZoneRef.name" // DnsRecordIndex is used for indexing and watching 34 | ValidationPassedIndex string = ".status.ValidationPassed" // ValidationPassedIndex is used for indexing and watching 35 | ) 36 | 37 | // Record defines DNS record. 38 | type Record struct { 39 | // name specifes the record name. 40 | Name string `json:"name"` 41 | 42 | // value is a value of the record 43 | Value string `json:"value"` 44 | 45 | // type is a record type according to RFC1035. 46 | // Supported types: A;AAAA;CNAME;MX;TXT;NS;PTR;SRV;CAA;DNSKEY;DS;NAPTR;RRSIG;DNAME;HINFO; 47 | // +kubebuilder:validation:Enum=A;AAAA;CNAME;MX;TXT;NS;PTR;SRV;CAA;DNSKEY;DS;NAPTR;RRSIG;DNAME;HINFO; 48 | Type string `json:"type"` 49 | 50 | // ttl is time to live, which tells how ling this record can be cached. 51 | // if not set, the default is minimumTTL value in the SOA record. 52 | // +kubebuilder:validation:Optional 53 | TTL string `json:"ttl,omitempty"` 54 | } 55 | 56 | type DNSRecordSpec struct { 57 | // Record defines the desired DNS record. 58 | Record *Record `json:"record"` 59 | 60 | // dnsZoneRef is a reference to a DNSZone instance to which this record will publish its endpoints. 61 | DNSZoneRef *corev1.ObjectReference `json:"dnsZoneRef"` 62 | } 63 | 64 | // DNSRecordStatus defines the observed state of DNSRecord. 65 | type DNSRecordStatus struct { 66 | // conditions indidicate the status of a DNSRecord. 67 | // +listType=map 68 | // +listMapKey=type 69 | // +optional 70 | Conditions []metav1.Condition `json:"conditions,omitempty"` 71 | 72 | // validationPassed displays whether the record passed syntax validation check 73 | ValidationPassed bool `json:"validationPassed,omitempty"` 74 | 75 | // generatedRecord displayes the generated dns record. 76 | GeneratedRecord string `json:"generatedRecord,omitempty"` 77 | } 78 | 79 | //+kubebuilder:object:root=true 80 | //+kubebuilder:subresource:status 81 | //+kubebuilder:printcolumn:name="Record Name",type="string",JSONPath=".spec.record.name",description="Record name" 82 | //+kubebuilder:printcolumn:name="Record Type",type="string",JSONPath=".spec.record.type",description="Record type" 83 | //+kubebuilder:printcolumn:name="Record Value",type="string",JSONPath=".spec.record.value",description="Record value" 84 | //+kubebuilder:printcolumn:name="Zone Reference",type="string",JSONPath=".spec.dnsZoneRef.name",description="Reference to the zone" 85 | //+kubebuilder:printcolumn:name="Last Change",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].lastTransitionTime",description="Last Change" 86 | //+kubebuilder:printcolumn:name="State",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].reason",description="DNSRecord State" 87 | 88 | // DNSRecord is the Schema for the dnsrecords API 89 | type DNSRecord struct { 90 | metav1.TypeMeta `json:",inline"` 91 | metav1.ObjectMeta `json:"metadata,omitempty"` 92 | 93 | Spec DNSRecordSpec `json:"spec,omitempty"` 94 | Status DNSRecordStatus `json:"status,omitempty"` 95 | } 96 | 97 | //+kubebuilder:object:root=true 98 | 99 | // DNSRecordList contains a list of DNSRecord 100 | type DNSRecordList struct { 101 | metav1.TypeMeta `json:",inline"` 102 | metav1.ListMeta `json:"metadata,omitempty"` 103 | Items []DNSRecord `json:"items"` 104 | } 105 | 106 | // EnsureFQDN ensures that the given name is a fully qualified domain name. 107 | func EnsureFQDN(name string) string { 108 | if !strings.HasSuffix(name, ".") { 109 | return name + "." 110 | } 111 | return name 112 | } 113 | 114 | func init() { 115 | SchemeBuilder.Register(&DNSRecord{}, &DNSRecordList{}) 116 | } 117 | -------------------------------------------------------------------------------- /api/v1alpha1/dnszone_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 monkale.io. 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 | http://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 | "time" 21 | 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | ) 24 | 25 | const ( 26 | ConditionZoneTypeReady string = "Ready" // ConditionZoneTypeReady is used to update condition type 27 | ConditionReasonZoneActive string = "Active" // ConditionReasonZoneActive represents state of the DNSZone 28 | ConditionReasonZonePending string = "Pending" // ConditionReasonRecordPending represents state of the DNSZone 29 | ConditionReasonZoneUpdateErr string = "UpdateError" // ConditionReasonZoneUpdateErr represents state of the DNSZone 30 | ConditionReasonZoneNoConnector string = "NoConnector" // ConditionReasonZoneNoConnector represents state of the DNSZone in which the zone has no connector 31 | ConditionReasonZoneUnknown string = "Unknown" // ConditionReasonZoneUnknown string = "Unknown" 32 | DnsZonesFinalizerName string = "dnszones/finalizers" // DnsZonesFinalizerName is finalizer used by DNSZone controller 33 | DnsZoneConnectorIndex string = "spec.ConnectorName" // DnsZoneConnectorIndex is used for indexing and watching 34 | ) 35 | 36 | // primaryNS defines the primary Nameserver for the DNSZone. 37 | // If the operator used to manage zones in offline deployments 38 | // the "name" could be negligenced. 39 | type PrimaryNS struct { 40 | // hostname is the server name of the primary name server for this zone. 41 | // The default value is "ns1". 42 | // +kubebuilder:default:=ns1 43 | // +kubebuilder:validation:Optional 44 | // +kubebuilder:validation:MinLength=1 45 | // +kubebuilder:validation:MaxLength=253 46 | // +kubebuilder:validation:Pattern=`^[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?)*$` 47 | Hostname string `json:"hostname"` 48 | 49 | // ipAddress defines IP address to the dns server where the zone hosted. 50 | // If the zone is managed by k8s coredns specify IP of kubernetes lb/node. 51 | // Provide either ipv4, or ipv6. 52 | IPAddress string `json:"ipAddress"` 53 | 54 | // recordType defines the type of the record to be created for the NS's A record. 55 | // In case of ipv6 set it to "AAAA". 56 | // The default value is "A". 57 | // +kubebuilder:default:=A 58 | // +kubebuilder:validation:Enum=A;AAAA; 59 | RecordType string `json:"recordType"` 60 | } 61 | 62 | // DNSZoneSpec defines the desired state of DNSZone. 63 | // DNSZoneSpec creates the new zone file with the SOA record. 64 | // DNSZoneSpec creates DNSRecords of type NS. 65 | type DNSZoneSpec struct { 66 | // cmPrefix specifies the prefix for the zone file configmap. 67 | // The default value is coredns-zone-. 68 | // The CM Name format is "prefix" + "metadata.name", 69 | // +kubebuilder:default:=coredns-zone- 70 | // +kubebuilder:validation:Optional 71 | // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(-)?$` 72 | CMPrefix string `json:"cmPrefix"` 73 | 74 | // domain specifies domain in which DNSRecors are valid. 75 | // +kubebuilder:validation:Required 76 | Domain string `json:"domain"` 77 | 78 | // primaryNS defines NS record for the zone, and its A/AAAA record. 79 | // +kubebuilder:validation:Required 80 | PrimaryNS *PrimaryNS `json:"primaryNS"` 81 | 82 | // respPersonEmail is responsible party's email for the domain. 83 | // Typically formatted as admin@example.com but represented with a dot (.) 84 | // instead of an at (@) in DNS records. The first dot separates the user name from the domain. 85 | // +kubebuilder:validation:Required 86 | // +kubebuilder:validation:Pattern=`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$` 87 | RespPersonEmail string `json:"respPersonEmail"` 88 | 89 | // ttl specified default Time to Lieve for the zone's records, indicates how long 90 | // these records should be cached by DNS resolvers. 91 | // The default value is 86400 seconds (24 hours) 92 | // +kubebuilder:default:=86400 93 | // +kubebuilder:validation:Optional 94 | TTL uint `json:"ttl,omitempty"` 95 | 96 | // refreshRate defines the time a secondary DNS server waits before querying the primary DNS 97 | // server to check for updates. 98 | // If the zone file has changed, secondary servers will refresh their data. 99 | // these records should be cached by DNS resolvers. 100 | // The default value is 7200 seconds (2 hours) 101 | // +kubebuilder:default:=7200 102 | // +kubebuilder:validation:Optional 103 | RefreshRate uint `json:"refreshRate,omitempty"` 104 | 105 | // retryInterval defines how long secondary server failed should wait 106 | // before trying again to reconnect to the primary again. 107 | // The default value is 3600 seconds (1 hour) 108 | // +kubebuilder:default:=3600 109 | // +kubebuilder:validation:Optional 110 | RetryInterval uint `json:"retryInterval,omitempty"` 111 | 112 | // expireTime defines how long the secondary server should wait 113 | // before discarding the zone data if it cannot reach the primary server. 114 | // The default value is 1209600 seconds (2 weeks) 115 | // +kubebuilder:default:=1209600 116 | // +kubebuilder:validation:Optional 117 | ExpireTime uint `json:"expireTime,omitempty"` 118 | 119 | // minimumTTL is the minimum amount of time that should be allowed for caching the DNS records. 120 | // If individual records do not specify a TTL, this value should be used. 121 | // The default value is 86400 seconds (24 hours) 122 | // +kubebuilder:default:=86400 123 | // +kubebuilder:validation:Optional 124 | MinimumTTL uint `json:"minimumTTL,omitempty"` 125 | 126 | // connectorName is the pointer to the DNSConnector Resource. 127 | // Must contain the name of the DNSConnector Resource. 128 | // +kubebuilder:validation:Required 129 | ConnectorName string `json:"connectorName,omitempty"` 130 | } 131 | 132 | // DNSZoneStatus defines the observed state of DNSZone 133 | type DNSZoneStatus struct { 134 | // conditions indidicate the status of a DNSZone. 135 | // +listType=map 136 | // +listMapKey=type 137 | // +optional 138 | Conditions []metav1.Condition `json:"conditions,omitempty"` 139 | 140 | // currentZoneSerial is a version number that changes update of the zone file, 141 | // signaling to secondary DNS servers when they should synchronize their data. 142 | // In our reality we use it to represent the zone file version. 143 | // Zone Serial implemented as time now formatted to MMDDHHMMSS. 144 | // Zone Serial represents the current version of the zone file. 145 | // +optional 146 | // +kubebuilder:default:="000000001" 147 | CurrentZoneSerial string `json:"currentZoneSerial,omitempty"` 148 | 149 | // recordCount is the number of records in the zone. 150 | // Does not include SOA and NS. 151 | // +optional 152 | // +kubebuilder:default:=0 153 | RecordCount int `json:"recordCount,omitempty"` 154 | 155 | // validationPassed displays whether the zonefile passed syntax validation check 156 | ValidationPassed bool `json:"validationPassed,omitempty"` 157 | 158 | // zoneConfigmap displays the name of the generated zone config map 159 | ZoneConfigmap string `json:"zoneConfigmap,omitempty"` 160 | 161 | // checkpoint flag indicates whether the DNSZone was previously active. 162 | // This flag is used to instruct the DNSConnector to preserve the old version of the DNSZone 163 | // in case the update process encounters an issue. 164 | Checkpoint bool `json:"checkpoint,omitempty"` 165 | } 166 | 167 | //+kubebuilder:object:root=true 168 | //+kubebuilder:subresource:status 169 | //+kubebuilder:printcolumn:name="Domain Name",type="string",JSONPath=".spec.domain",description="Domain name" 170 | //+kubebuilder:printcolumn:name="Record Count",type="integer",JSONPath=".status.recordCount",description="Record Count. Without SOA and First NS" 171 | //+kubebuilder:printcolumn:name="Last Change",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].lastTransitionTime",description="Last Change" 172 | //+kubebuilder:printcolumn:name="Current Serial",type="string",JSONPath=".status.currentZoneSerial",description="Represents the current version of the zonefile" 173 | //+kubebuilder:printcolumn:name="State",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].reason",description="DNSZone state" 174 | 175 | // DNSZone is the Schema for the dnszones API 176 | type DNSZone struct { 177 | metav1.TypeMeta `json:",inline"` 178 | metav1.ObjectMeta `json:"metadata,omitempty"` 179 | 180 | Spec DNSZoneSpec `json:"spec,omitempty"` 181 | Status DNSZoneStatus `json:"status,omitempty"` 182 | } 183 | 184 | //+kubebuilder:object:root=true 185 | 186 | // DNSZoneList contains a list of DNSZone 187 | type DNSZoneList struct { 188 | metav1.TypeMeta `json:",inline"` 189 | metav1.ListMeta `json:"metadata,omitempty"` 190 | Items []DNSZone `json:"items"` 191 | } 192 | 193 | // DNSZoneHeader represents the minimal Zonefile: SOA + First NS records. 194 | // values needed to define the SOA, its NS and A records. 195 | type DNSZoneHeader struct { 196 | DomainName string // Zone origin 197 | PrimaryNSHostname string // Primary nameserver hostname 198 | PrimaryNSIp string // Primary nameserver IP 199 | PrimaryNSType string // Primary nameserver record type: A or AAAA 200 | RespPerson string // Responsible person's email 201 | Serial string // Serial number 202 | ZoneTTL uint // Zone ttl 203 | Refresh uint // Refresh time 204 | Retry uint // Retry time 205 | Expire uint // Expire time 206 | MinimumTTL uint // Minimum TTL 207 | } 208 | 209 | // DNSZoneGenerateSerial generates serial number in format using time formatted to MMDDHHMMSS 210 | func DNSZoneGenerateSerial() (string, error) { 211 | now := time.Now() 212 | timePart := now.Format("0102150405") 213 | 214 | // Ensure that the final serial number is a string 215 | return timePart, nil 216 | } 217 | 218 | func init() { 219 | SchemeBuilder.Register(&DNSZone{}, &DNSZoneList{}) 220 | } 221 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 monkale.io. 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 | http://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 monkale v1alpha1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=monkale.monkale.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: "monkale.monkale.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 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 monkale.io. 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 | http://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 | "os" 22 | 23 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 24 | // to ensure that exec-entrypoint and run can make use of them. 25 | _ "k8s.io/client-go/plugin/pkg/client/auth" 26 | 27 | "k8s.io/apimachinery/pkg/runtime" 28 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 29 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 30 | ctrl "sigs.k8s.io/controller-runtime" 31 | "sigs.k8s.io/controller-runtime/pkg/healthz" 32 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 33 | 34 | monkalev1alpha1 "github.com/monkale.io/coredns-manager-operator/api/v1alpha1" 35 | "github.com/monkale.io/coredns-manager-operator/internal/controller" 36 | //+kubebuilder:scaffold:imports 37 | ) 38 | 39 | var ( 40 | scheme = runtime.NewScheme() 41 | setupLog = ctrl.Log.WithName("setup") 42 | ) 43 | 44 | func init() { 45 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 46 | 47 | utilruntime.Must(monkalev1alpha1.AddToScheme(scheme)) 48 | //+kubebuilder:scaffold:scheme 49 | } 50 | 51 | func main() { 52 | var metricsAddr string 53 | var enableLeaderElection bool 54 | var probeAddr string 55 | flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") 56 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 57 | flag.BoolVar(&enableLeaderElection, "leader-elect", false, 58 | "Enable leader election for controller manager. "+ 59 | "Enabling this will ensure there is only one active controller manager.") 60 | opts := zap.Options{ 61 | Development: false, 62 | } 63 | opts.BindFlags(flag.CommandLine) 64 | flag.Parse() 65 | 66 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 67 | 68 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 69 | Scheme: scheme, 70 | MetricsBindAddress: metricsAddr, 71 | Port: 9443, 72 | HealthProbeBindAddress: probeAddr, 73 | LeaderElection: enableLeaderElection, 74 | LeaderElectionID: "7aca862b.monkale.io", 75 | // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily 76 | // when the Manager ends. This requires the binary to immediately end when the 77 | // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly 78 | // speeds up voluntary leader transitions as the new leader don't have to wait 79 | // LeaseDuration time first. 80 | // 81 | // In the default scaffold provided, the program ends immediately after 82 | // the manager stops, so would be fine to enable this option. However, 83 | // if you are doing or is intended to do any operation such as perform cleanups 84 | // after the manager stops then its usage might be unsafe. 85 | // LeaderElectionReleaseOnCancel: true, 86 | }) 87 | if err != nil { 88 | setupLog.Error(err, "unable to start manager") 89 | os.Exit(1) 90 | } 91 | 92 | if err = (&controller.DNSZoneReconciler{ 93 | Client: mgr.GetClient(), 94 | Scheme: mgr.GetScheme(), 95 | }).SetupWithManager(mgr); err != nil { 96 | setupLog.Error(err, "unable to create controller", "controller", "DNSZone") 97 | os.Exit(1) 98 | } 99 | if err = (&controller.DNSRecordReconciler{ 100 | Client: mgr.GetClient(), 101 | Scheme: mgr.GetScheme(), 102 | }).SetupWithManager(mgr); err != nil { 103 | setupLog.Error(err, "unable to create controller", "controller", "DNSRecord") 104 | os.Exit(1) 105 | } 106 | if err = (&controller.DNSConnectorReconciler{ 107 | Client: mgr.GetClient(), 108 | Scheme: mgr.GetScheme(), 109 | }).SetupWithManager(mgr); err != nil { 110 | setupLog.Error(err, "unable to create controller", "controller", "DNSConnector") 111 | os.Exit(1) 112 | } 113 | //+kubebuilder:scaffold:builder 114 | 115 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 116 | setupLog.Error(err, "unable to set up health check") 117 | os.Exit(1) 118 | } 119 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 120 | setupLog.Error(err, "unable to set up ready check") 121 | os.Exit(1) 122 | } 123 | 124 | setupLog.Info("starting manager") 125 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 126 | setupLog.Error(err, "problem running manager") 127 | os.Exit(1) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /config/crd/bases/monkale.monkale.io_dnsconnectors.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.12.0 7 | name: dnsconnectors.monkale.monkale.io 8 | spec: 9 | group: monkale.monkale.io 10 | names: 11 | kind: DNSConnector 12 | listKind: DNSConnectorList 13 | plural: dnsconnectors 14 | singular: dnsconnector 15 | scope: Namespaced 16 | versions: 17 | - additionalPrinterColumns: 18 | - description: Last Change 19 | jsonPath: .status.conditions[?(@.type=="Ready")].lastTransitionTime 20 | name: Last Change 21 | type: string 22 | - description: The current state 23 | jsonPath: .status.conditions[?(@.type=="Ready")].reason 24 | name: State 25 | type: string 26 | - description: The current state 27 | jsonPath: .status.conditions[?(@.type=="Ready")].message 28 | name: Message 29 | type: string 30 | name: v1alpha1 31 | schema: 32 | openAPIV3Schema: 33 | description: DNSConnector is the Schema for the dnsconnectors API 34 | properties: 35 | apiVersion: 36 | description: 'APIVersion defines the versioned schema of this representation 37 | of an object. Servers should convert recognized schemas to the latest 38 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 39 | type: string 40 | kind: 41 | description: 'Kind is a string value representing the REST resource this 42 | object represents. Servers may infer this from the endpoint the client 43 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 44 | type: string 45 | metadata: 46 | type: object 47 | spec: 48 | description: DNSConnectorSpec defines the desired state of DNSConnector 49 | properties: 50 | corednsCM: 51 | description: corednsCM is the name of the CoreDNS ConfigMap. 52 | properties: 53 | corefileKey: 54 | default: Corefile 55 | description: corefileKey specifies the key whose value is the 56 | Corefile. Typically, this key is "Corefile". The default value 57 | is "Corefile" 58 | type: string 59 | name: 60 | default: coredns 61 | type: string 62 | type: object 63 | corednsDeployment: 64 | description: corednsDeployment specifies the CoreDNS deployment type 65 | and name or labels 66 | properties: 67 | name: 68 | description: name specifies the name of the CoreDNS resource. 69 | This field is optional if Type is Pod and a LabelSelector is 70 | specified. 71 | type: string 72 | type: 73 | description: type of the CoreDNS resource (e.g., Deployment, StatefulSet, 74 | DaemonSet, ReplicaSet, Pod) 75 | enum: 76 | - Deployment 77 | - StatefulSet 78 | - DaemonSet 79 | type: string 80 | zonefilesMountDir: 81 | default: /opt/coredns 82 | description: zonefilesMountDir specifies the mountPath for zonefiles. 83 | Default value is /opt/coredns. 84 | pattern: ^(/[^/]+)+$ 85 | type: string 86 | required: 87 | - type 88 | type: object 89 | corednsZoneEnaledPlugins: 90 | description: 'corednsZoneEnaledPlugins is list of enabled coredns 91 | plugins. https://coredns.io/plugins. The most useful plugins are: 92 | errors - prints errors to stdout; log - prints queries to stdout.' 93 | items: 94 | type: string 95 | type: array 96 | waitForUpdateTimeout: 97 | default: 120 98 | description: waitForUpdateTimeout specifies how long the DNSConnector 99 | for coredns to complete update. if coredns deployment haven't complete 100 | the update, the controller will perform rollback. The default value 101 | is 120 seconds (2 min) 102 | type: integer 103 | required: 104 | - corednsCM 105 | - corednsDeployment 106 | - waitForUpdateTimeout 107 | type: object 108 | status: 109 | description: DNSConnectorStatus defines the observed state of DNSConnector 110 | properties: 111 | conditions: 112 | description: conditions indidicate the status of a DNSZone. 113 | items: 114 | description: "Condition contains details for one aspect of the current 115 | state of this API Resource. --- This struct is intended for direct 116 | use as an array at the field path .status.conditions. For example, 117 | \n type FooStatus struct{ // Represents the observations of a 118 | foo's current state. // Known .status.conditions.type are: \"Available\", 119 | \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge 120 | // +listType=map // +listMapKey=type Conditions []metav1.Condition 121 | `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" 122 | protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" 123 | properties: 124 | lastTransitionTime: 125 | description: lastTransitionTime is the last time the condition 126 | transitioned from one status to another. This should be when 127 | the underlying condition changed. If that is not known, then 128 | using the time when the API field changed is acceptable. 129 | format: date-time 130 | type: string 131 | message: 132 | description: message is a human readable message indicating 133 | details about the transition. This may be an empty string. 134 | maxLength: 32768 135 | type: string 136 | observedGeneration: 137 | description: observedGeneration represents the .metadata.generation 138 | that the condition was set based upon. For instance, if .metadata.generation 139 | is currently 12, but the .status.conditions[x].observedGeneration 140 | is 9, the condition is out of date with respect to the current 141 | state of the instance. 142 | format: int64 143 | minimum: 0 144 | type: integer 145 | reason: 146 | description: reason contains a programmatic identifier indicating 147 | the reason for the condition's last transition. Producers 148 | of specific condition types may define expected values and 149 | meanings for this field, and whether the values are considered 150 | a guaranteed API. The value should be a CamelCase string. 151 | This field may not be empty. 152 | maxLength: 1024 153 | minLength: 1 154 | pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ 155 | type: string 156 | status: 157 | description: status of the condition, one of True, False, Unknown. 158 | enum: 159 | - "True" 160 | - "False" 161 | - Unknown 162 | type: string 163 | type: 164 | description: type of condition in CamelCase or in foo.example.com/CamelCase. 165 | --- Many .condition.type values are consistent across resources 166 | like Available, but because arbitrary conditions can be useful 167 | (see .node.status.conditions), the ability to deconflict is 168 | important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) 169 | maxLength: 316 170 | pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ 171 | type: string 172 | required: 173 | - lastTransitionTime 174 | - message 175 | - reason 176 | - status 177 | - type 178 | type: object 179 | type: array 180 | x-kubernetes-list-map-keys: 181 | - type 182 | x-kubernetes-list-type: map 183 | provisionedZones: 184 | description: provisionedZones maps domain names to their serial numbers. 185 | items: 186 | description: ProvisionedDNSZone used to display the status of the 187 | zones provisioned to the Coredns 188 | properties: 189 | domain: 190 | type: string 191 | name: 192 | type: string 193 | serialNumber: 194 | type: string 195 | required: 196 | - domain 197 | - name 198 | - serialNumber 199 | type: object 200 | type: array 201 | type: object 202 | type: object 203 | served: true 204 | storage: true 205 | subresources: 206 | status: {} 207 | -------------------------------------------------------------------------------- /config/crd/bases/monkale.monkale.io_dnsrecords.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.12.0 7 | name: dnsrecords.monkale.monkale.io 8 | spec: 9 | group: monkale.monkale.io 10 | names: 11 | kind: DNSRecord 12 | listKind: DNSRecordList 13 | plural: dnsrecords 14 | singular: dnsrecord 15 | scope: Namespaced 16 | versions: 17 | - additionalPrinterColumns: 18 | - description: Record name 19 | jsonPath: .spec.record.name 20 | name: Record Name 21 | type: string 22 | - description: Record type 23 | jsonPath: .spec.record.type 24 | name: Record Type 25 | type: string 26 | - description: Record value 27 | jsonPath: .spec.record.value 28 | name: Record Value 29 | type: string 30 | - description: Reference to the zone 31 | jsonPath: .spec.dnsZoneRef.name 32 | name: Zone Reference 33 | type: string 34 | - description: Last Change 35 | jsonPath: .status.conditions[?(@.type=='Ready')].lastTransitionTime 36 | name: Last Change 37 | type: string 38 | - description: DNSRecord State 39 | jsonPath: .status.conditions[?(@.type=='Ready')].reason 40 | name: State 41 | type: string 42 | name: v1alpha1 43 | schema: 44 | openAPIV3Schema: 45 | description: DNSRecord is the Schema for the dnsrecords API 46 | properties: 47 | apiVersion: 48 | description: 'APIVersion defines the versioned schema of this representation 49 | of an object. Servers should convert recognized schemas to the latest 50 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 51 | type: string 52 | kind: 53 | description: 'Kind is a string value representing the REST resource this 54 | object represents. Servers may infer this from the endpoint the client 55 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 56 | type: string 57 | metadata: 58 | type: object 59 | spec: 60 | properties: 61 | dnsZoneRef: 62 | description: dnsZoneRef is a reference to a DNSZone instance to which 63 | this record will publish its endpoints. 64 | properties: 65 | apiVersion: 66 | description: API version of the referent. 67 | type: string 68 | fieldPath: 69 | description: 'If referring to a piece of an object instead of 70 | an entire object, this string should contain a valid JSON/Go 71 | field access statement, such as desiredState.manifest.containers[2]. 72 | For example, if the object reference is to a container within 73 | a pod, this would take on a value like: "spec.containers{name}" 74 | (where "name" refers to the name of the container that triggered 75 | the event) or if no container name is specified "spec.containers[2]" 76 | (container with index 2 in this pod). This syntax is chosen 77 | only to have some well-defined way of referencing a part of 78 | an object. TODO: this design is not final and this field is 79 | subject to change in the future.' 80 | type: string 81 | kind: 82 | description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 83 | type: string 84 | name: 85 | description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' 86 | type: string 87 | namespace: 88 | description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' 89 | type: string 90 | resourceVersion: 91 | description: 'Specific resourceVersion to which this reference 92 | is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' 93 | type: string 94 | uid: 95 | description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' 96 | type: string 97 | type: object 98 | x-kubernetes-map-type: atomic 99 | record: 100 | description: Record defines the desired DNS record. 101 | properties: 102 | name: 103 | description: name specifes the record name. 104 | type: string 105 | ttl: 106 | description: ttl is time to live, which tells how ling this record 107 | can be cached. if not set, the default is minimumTTL value in 108 | the SOA record. 109 | type: string 110 | type: 111 | description: 'type is a record type according to RFC1035. Supported 112 | types: A;AAAA;CNAME;MX;TXT;NS;PTR;SRV;CAA;DNSKEY;DS;NAPTR;RRSIG;DNAME;HINFO;' 113 | enum: 114 | - A 115 | - AAAA 116 | - CNAME 117 | - MX 118 | - TXT 119 | - NS 120 | - PTR 121 | - SRV 122 | - CAA 123 | - DNSKEY 124 | - DS 125 | - NAPTR 126 | - RRSIG 127 | - DNAME 128 | - HINFO 129 | type: string 130 | value: 131 | description: value is a value of the record 132 | type: string 133 | required: 134 | - name 135 | - type 136 | - value 137 | type: object 138 | required: 139 | - dnsZoneRef 140 | - record 141 | type: object 142 | status: 143 | description: DNSRecordStatus defines the observed state of DNSRecord. 144 | properties: 145 | conditions: 146 | description: conditions indidicate the status of a DNSRecord. 147 | items: 148 | description: "Condition contains details for one aspect of the current 149 | state of this API Resource. --- This struct is intended for direct 150 | use as an array at the field path .status.conditions. For example, 151 | \n type FooStatus struct{ // Represents the observations of a 152 | foo's current state. // Known .status.conditions.type are: \"Available\", 153 | \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge 154 | // +listType=map // +listMapKey=type Conditions []metav1.Condition 155 | `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" 156 | protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" 157 | properties: 158 | lastTransitionTime: 159 | description: lastTransitionTime is the last time the condition 160 | transitioned from one status to another. This should be when 161 | the underlying condition changed. If that is not known, then 162 | using the time when the API field changed is acceptable. 163 | format: date-time 164 | type: string 165 | message: 166 | description: message is a human readable message indicating 167 | details about the transition. This may be an empty string. 168 | maxLength: 32768 169 | type: string 170 | observedGeneration: 171 | description: observedGeneration represents the .metadata.generation 172 | that the condition was set based upon. For instance, if .metadata.generation 173 | is currently 12, but the .status.conditions[x].observedGeneration 174 | is 9, the condition is out of date with respect to the current 175 | state of the instance. 176 | format: int64 177 | minimum: 0 178 | type: integer 179 | reason: 180 | description: reason contains a programmatic identifier indicating 181 | the reason for the condition's last transition. Producers 182 | of specific condition types may define expected values and 183 | meanings for this field, and whether the values are considered 184 | a guaranteed API. The value should be a CamelCase string. 185 | This field may not be empty. 186 | maxLength: 1024 187 | minLength: 1 188 | pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ 189 | type: string 190 | status: 191 | description: status of the condition, one of True, False, Unknown. 192 | enum: 193 | - "True" 194 | - "False" 195 | - Unknown 196 | type: string 197 | type: 198 | description: type of condition in CamelCase or in foo.example.com/CamelCase. 199 | --- Many .condition.type values are consistent across resources 200 | like Available, but because arbitrary conditions can be useful 201 | (see .node.status.conditions), the ability to deconflict is 202 | important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) 203 | maxLength: 316 204 | pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ 205 | type: string 206 | required: 207 | - lastTransitionTime 208 | - message 209 | - reason 210 | - status 211 | - type 212 | type: object 213 | type: array 214 | x-kubernetes-list-map-keys: 215 | - type 216 | x-kubernetes-list-type: map 217 | generatedRecord: 218 | description: generatedRecord displayes the generated dns record. 219 | type: string 220 | validationPassed: 221 | description: validationPassed displays whether the record passed syntax 222 | validation check 223 | type: boolean 224 | type: object 225 | type: object 226 | served: true 227 | storage: true 228 | subresources: 229 | status: {} 230 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/monkale.monkale.io_dnszones.yaml 6 | - bases/monkale.monkale.io_dnsrecords.yaml 7 | - bases/monkale.monkale.io_dnsconnectors.yaml 8 | #+kubebuilder:scaffold:crdkustomizeresource 9 | 10 | patches: 11 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 12 | # patches here are for enabling the conversion webhook for each CRD 13 | #- path: patches/webhook_in_dnszones.yaml 14 | #- path: patches/webhook_in_dnsrecords.yaml 15 | #- path: patches/webhook_in_dnsconnectors.yaml 16 | #+kubebuilder:scaffold:crdkustomizewebhookpatch 17 | 18 | # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. 19 | # patches here are for enabling the CA injection for each CRD 20 | #- path: patches/cainjection_in_dnszones.yaml 21 | #- path: patches/cainjection_in_dnsrecords.yaml 22 | #- path: patches/cainjection_in_dnsconnectors.yaml 23 | #+kubebuilder:scaffold:crdkustomizecainjectionpatch 24 | 25 | # the following config is for teaching kustomize how to do kustomization for CRDs. 26 | configurations: 27 | - kustomizeconfig.yaml 28 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_dnsconnectors.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME 7 | name: dnsconnectors.monkale.monkale.io 8 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_dnsrecords.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME 7 | name: dnsrecords.monkale.monkale.io 8 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_dnszones.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME 7 | name: dnszones.monkale.monkale.io 8 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_dnsconnectors.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: dnsconnectors.monkale.monkale.io 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_dnsrecords.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: dnsrecords.monkale.monkale.io 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_dnszones.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: dnszones.monkale.monkale.io 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: kube-system 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: coredns-manager-operator- 10 | 11 | # Labels to add to all resources and selectors. 12 | #labels: 13 | #- includeSelectors: true 14 | # pairs: 15 | # someName: someValue 16 | 17 | resources: 18 | - ../crd 19 | - ../rbac 20 | - ../manager 21 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 22 | # crd/kustomization.yaml 23 | #- ../webhook 24 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 25 | #- ../certmanager 26 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 27 | #- ../prometheus 28 | 29 | patchesStrategicMerge: 30 | # Protect the /metrics endpoint by putting it behind auth. 31 | # If you want your controller-manager to expose the /metrics 32 | # endpoint w/o any authn/z, please comment the following line. 33 | - manager_auth_proxy_patch.yaml 34 | 35 | 36 | 37 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 38 | # crd/kustomization.yaml 39 | #- manager_webhook_patch.yaml 40 | 41 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 42 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 43 | # 'CERTMANAGER' needs to be enabled to use ca injection 44 | #- webhookcainjection_patch.yaml 45 | 46 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 47 | # Uncomment the following replacements to add the cert-manager CA injection annotations 48 | #replacements: 49 | # - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs 50 | # kind: Certificate 51 | # group: cert-manager.io 52 | # version: v1 53 | # name: serving-cert # this name should match the one in certificate.yaml 54 | # fieldPath: .metadata.namespace # namespace of the certificate CR 55 | # targets: 56 | # - select: 57 | # kind: ValidatingWebhookConfiguration 58 | # fieldPaths: 59 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 60 | # options: 61 | # delimiter: '/' 62 | # index: 0 63 | # create: true 64 | # - select: 65 | # kind: MutatingWebhookConfiguration 66 | # fieldPaths: 67 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 68 | # options: 69 | # delimiter: '/' 70 | # index: 0 71 | # create: true 72 | # - select: 73 | # kind: CustomResourceDefinition 74 | # fieldPaths: 75 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 76 | # options: 77 | # delimiter: '/' 78 | # index: 0 79 | # create: true 80 | # - source: 81 | # kind: Certificate 82 | # group: cert-manager.io 83 | # version: v1 84 | # name: serving-cert # this name should match the one in certificate.yaml 85 | # fieldPath: .metadata.name 86 | # targets: 87 | # - select: 88 | # kind: ValidatingWebhookConfiguration 89 | # fieldPaths: 90 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 91 | # options: 92 | # delimiter: '/' 93 | # index: 1 94 | # create: true 95 | # - select: 96 | # kind: MutatingWebhookConfiguration 97 | # fieldPaths: 98 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 99 | # options: 100 | # delimiter: '/' 101 | # index: 1 102 | # create: true 103 | # - select: 104 | # kind: CustomResourceDefinition 105 | # fieldPaths: 106 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 107 | # options: 108 | # delimiter: '/' 109 | # index: 1 110 | # create: true 111 | # - source: # Add cert-manager annotation to the webhook Service 112 | # kind: Service 113 | # version: v1 114 | # name: webhook-service 115 | # fieldPath: .metadata.name # namespace of the service 116 | # targets: 117 | # - select: 118 | # kind: Certificate 119 | # group: cert-manager.io 120 | # version: v1 121 | # fieldPaths: 122 | # - .spec.dnsNames.0 123 | # - .spec.dnsNames.1 124 | # options: 125 | # delimiter: '.' 126 | # index: 0 127 | # create: true 128 | # - source: 129 | # kind: Service 130 | # version: v1 131 | # name: webhook-service 132 | # fieldPath: .metadata.namespace # namespace of the service 133 | # targets: 134 | # - select: 135 | # kind: Certificate 136 | # group: cert-manager.io 137 | # version: v1 138 | # fieldPaths: 139 | # - .spec.dnsNames.0 140 | # - .spec.dnsNames.1 141 | # options: 142 | # delimiter: '.' 143 | # index: 1 144 | # create: true 145 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: kube-rbac-proxy 13 | securityContext: 14 | allowPrivilegeEscalation: false 15 | capabilities: 16 | drop: 17 | - "ALL" 18 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.14.1 19 | args: 20 | - "--secure-listen-address=0.0.0.0:8443" 21 | - "--upstream=http://127.0.0.1:8080/" 22 | - "--logtostderr=true" 23 | - "--v=0" 24 | ports: 25 | - containerPort: 8443 26 | protocol: TCP 27 | name: https 28 | resources: 29 | limits: 30 | cpu: 500m 31 | memory: 128Mi 32 | requests: 33 | cpu: 5m 34 | memory: 64Mi 35 | - name: manager 36 | args: 37 | - "--health-probe-bind-address=:8081" 38 | - "--metrics-bind-address=127.0.0.1:8080" 39 | - "--leader-elect" 40 | -------------------------------------------------------------------------------- /config/default/manager_config_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | images: 6 | - name: controller 7 | newName: docker.io/monkale/coredns-manager-operator 8 | newTag: v1.0.3 9 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: namespace 7 | app.kubernetes.io/instance: system 8 | app.kubernetes.io/component: manager 9 | app.kubernetes.io/created-by: coredns-manager-operator 10 | app.kubernetes.io/part-of: coredns-manager-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: system 13 | --- 14 | apiVersion: apps/v1 15 | kind: Deployment 16 | metadata: 17 | name: controller-manager 18 | namespace: system 19 | labels: 20 | control-plane: controller-manager 21 | app.kubernetes.io/name: deployment 22 | app.kubernetes.io/instance: controller-manager 23 | app.kubernetes.io/component: manager 24 | app.kubernetes.io/created-by: coredns-manager-operator 25 | app.kubernetes.io/part-of: coredns-manager-operator 26 | app.kubernetes.io/managed-by: kustomize 27 | spec: 28 | selector: 29 | matchLabels: 30 | control-plane: controller-manager 31 | replicas: 1 32 | template: 33 | metadata: 34 | annotations: 35 | kubectl.kubernetes.io/default-container: manager 36 | labels: 37 | control-plane: controller-manager 38 | spec: 39 | # TODO(user): Uncomment the following code to configure the nodeAffinity expression 40 | # according to the platforms which are supported by your solution. 41 | # It is considered best practice to support multiple architectures. You can 42 | # build your manager image using the makefile target docker-buildx. 43 | # affinity: 44 | # nodeAffinity: 45 | # requiredDuringSchedulingIgnoredDuringExecution: 46 | # nodeSelectorTerms: 47 | # - matchExpressions: 48 | # - key: kubernetes.io/arch 49 | # operator: In 50 | # values: 51 | # - amd64 52 | # - arm64 53 | # - ppc64le 54 | # - s390x 55 | # - key: kubernetes.io/os 56 | # operator: In 57 | # values: 58 | # - linux 59 | securityContext: 60 | runAsNonRoot: true 61 | # TODO(user): For common cases that do not require escalating privileges 62 | # it is recommended to ensure that all your Pods/Containers are restrictive. 63 | # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted 64 | # Please uncomment the following code if your project does NOT have to work on old Kubernetes 65 | # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). 66 | # seccompProfile: 67 | # type: RuntimeDefault 68 | containers: 69 | - command: 70 | - /manager 71 | args: 72 | - --leader-elect 73 | image: controller:latest 74 | name: manager 75 | securityContext: 76 | allowPrivilegeEscalation: false 77 | capabilities: 78 | drop: 79 | - "ALL" 80 | livenessProbe: 81 | httpGet: 82 | path: /healthz 83 | port: 8081 84 | initialDelaySeconds: 15 85 | periodSeconds: 20 86 | readinessProbe: 87 | httpGet: 88 | path: /readyz 89 | port: 8081 90 | initialDelaySeconds: 5 91 | periodSeconds: 10 92 | # TODO(user): Configure the resources accordingly based on the project requirements. 93 | # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 94 | resources: 95 | limits: 96 | cpu: 500m 97 | memory: 128Mi 98 | requests: 99 | cpu: 10m 100 | memory: 64Mi 101 | serviceAccountName: controller-manager 102 | terminationGracePeriodSeconds: 10 103 | -------------------------------------------------------------------------------- /config/manifests/bases/coredns-manager-operator.clusterserviceversion.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: operators.coreos.com/v1alpha1 2 | kind: ClusterServiceVersion 3 | metadata: 4 | annotations: 5 | alm-examples: '[]' 6 | capabilities: Basic Install 7 | name: coredns-manager-operator.v0.0.0 8 | namespace: placeholder 9 | spec: 10 | apiservicedefinitions: {} 11 | customresourcedefinitions: 12 | owned: 13 | - description: DNSConnector is the Schema for the dnsconnectors API 14 | displayName: DNSConnector 15 | kind: DNSConnector 16 | name: dnsconnectors.monkale.monkale.io 17 | version: v1alpha1 18 | - description: DNSRecord is the Schema for the dnsrecords API 19 | displayName: DNSRecord 20 | kind: DNSRecord 21 | name: dnsrecords.monkale.monkale.io 22 | version: v1alpha1 23 | - description: DNSZone is the Schema for the dnszones API 24 | displayName: DNSZone 25 | kind: DNSZone 26 | name: dnszones.monkale.monkale.io 27 | version: v1alpha1 28 | description: The CoreDNS Manager Operator enables Kubernetes to function as a standalone 29 | DNS server, ideal for offline or home lab environments. It eliminates the need 30 | for additional DNS software like named or dnsmasq and supports a GitOps approach 31 | for managing DNS configurations. 32 | displayName: coredns-manager-operator 33 | icon: 34 | - base64data: "" 35 | mediatype: "" 36 | install: 37 | spec: 38 | deployments: null 39 | strategy: "" 40 | installModes: 41 | - supported: false 42 | type: OwnNamespace 43 | - supported: false 44 | type: SingleNamespace 45 | - supported: false 46 | type: MultiNamespace 47 | - supported: true 48 | type: AllNamespaces 49 | keywords: 50 | - coredns manager 51 | - coredns operator 52 | - dnsrecord 53 | - dnszone 54 | - internal dns 55 | - dns gitops 56 | links: 57 | - name: Coredns Manager Operator 58 | url: https://coredns-manager-operator.domain 59 | maturity: alpha 60 | provider: 61 | name: monkale.io 62 | url: https://github.com/monkale-io/coredns-manager-operator 63 | version: 0.0.0 64 | -------------------------------------------------------------------------------- /config/manifests/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # These resources constitute the fully configured set of manifests 2 | # used to generate the 'manifests/' directory in a bundle. 3 | resources: 4 | - bases/coredns-manager-operator.clusterserviceversion.yaml 5 | - ../default 6 | - ../samples 7 | - ../scorecard 8 | 9 | # [WEBHOOK] To enable webhooks, uncomment all the sections with [WEBHOOK] prefix. 10 | # Do NOT uncomment sections with prefix [CERTMANAGER], as OLM does not support cert-manager. 11 | # These patches remove the unnecessary "cert" volume and its manager container volumeMount. 12 | #patchesJson6902: 13 | #- target: 14 | # group: apps 15 | # version: v1 16 | # kind: Deployment 17 | # name: controller-manager 18 | # namespace: system 19 | # patch: |- 20 | # # Remove the manager container's "cert" volumeMount, since OLM will create and mount a set of certs. 21 | # # Update the indices in this path if adding or removing containers/volumeMounts in the manager's Deployment. 22 | # - op: remove 23 | 24 | # path: /spec/template/spec/containers/0/volumeMounts/0 25 | # # Remove the "cert" volume, since OLM will create and mount a set of certs. 26 | # # Update the indices in this path if adding or removing volumes in the manager's Deployment. 27 | # - op: remove 28 | # path: /spec/template/spec/volumes/0 29 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: 7 | control-plane: controller-manager 8 | app.kubernetes.io/name: servicemonitor 9 | app.kubernetes.io/instance: controller-manager-metrics-monitor 10 | app.kubernetes.io/component: metrics 11 | app.kubernetes.io/created-by: coredns-manager-operator 12 | app.kubernetes.io/part-of: coredns-manager-operator 13 | app.kubernetes.io/managed-by: kustomize 14 | name: controller-manager-metrics-monitor 15 | namespace: system 16 | spec: 17 | endpoints: 18 | - path: /metrics 19 | port: https 20 | scheme: https 21 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 22 | tlsConfig: 23 | insecureSkipVerify: true 24 | selector: 25 | matchLabels: 26 | control-plane: controller-manager 27 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: metrics-reader 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: coredns-manager-operator 9 | app.kubernetes.io/part-of: coredns-manager-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: metrics-reader 12 | rules: 13 | - nonResourceURLs: 14 | - "/metrics" 15 | verbs: 16 | - get 17 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: proxy-role 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: coredns-manager-operator 9 | app.kubernetes.io/part-of: coredns-manager-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: proxy-role 12 | rules: 13 | - apiGroups: 14 | - authentication.k8s.io 15 | resources: 16 | - tokenreviews 17 | verbs: 18 | - create 19 | - apiGroups: 20 | - authorization.k8s.io 21 | resources: 22 | - subjectaccessreviews 23 | verbs: 24 | - create 25 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: proxy-rolebinding 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: coredns-manager-operator 9 | app.kubernetes.io/part-of: coredns-manager-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: proxy-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: proxy-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: service 7 | app.kubernetes.io/instance: controller-manager-metrics-service 8 | app.kubernetes.io/component: kube-rbac-proxy 9 | app.kubernetes.io/created-by: coredns-manager-operator 10 | app.kubernetes.io/part-of: coredns-manager-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: controller-manager-metrics-service 13 | namespace: system 14 | spec: 15 | ports: 16 | - name: https 17 | port: 8443 18 | protocol: TCP 19 | targetPort: https 20 | selector: 21 | control-plane: controller-manager 22 | -------------------------------------------------------------------------------- /config/rbac/dnsconnector_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit dnsconnectors. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: dnsconnector-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: coredns-manager-operator 10 | app.kubernetes.io/part-of: coredns-manager-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: dnsconnector-editor-role 13 | rules: 14 | - apiGroups: 15 | - monkale.monkale.io 16 | resources: 17 | - dnsconnectors 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - monkale.monkale.io 28 | resources: 29 | - dnsconnectors/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /config/rbac/dnsconnector_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view dnsconnectors. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: dnsconnector-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: coredns-manager-operator 10 | app.kubernetes.io/part-of: coredns-manager-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: dnsconnector-viewer-role 13 | rules: 14 | - apiGroups: 15 | - monkale.monkale.io 16 | resources: 17 | - dnsconnectors 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - monkale.monkale.io 24 | resources: 25 | - dnsconnectors/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /config/rbac/dnsrecord_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit dnsrecords. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: dnsrecord-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: coredns-manager-operator 10 | app.kubernetes.io/part-of: coredns-manager-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: dnsrecord-editor-role 13 | rules: 14 | - apiGroups: 15 | - monkale.monkale.io 16 | resources: 17 | - dnsrecords 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - monkale.monkale.io 28 | resources: 29 | - dnsrecords/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /config/rbac/dnsrecord_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view dnsrecords. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: dnsrecord-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: coredns-manager-operator 10 | app.kubernetes.io/part-of: coredns-manager-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: dnsrecord-viewer-role 13 | rules: 14 | - apiGroups: 15 | - monkale.monkale.io 16 | resources: 17 | - dnsrecords 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - monkale.monkale.io 24 | resources: 25 | - dnsrecords/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /config/rbac/dnszone_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit dnszones. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: dnszone-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: coredns-manager-operator 10 | app.kubernetes.io/part-of: coredns-manager-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: dnszone-editor-role 13 | rules: 14 | - apiGroups: 15 | - monkale.monkale.io 16 | resources: 17 | - dnszones 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - monkale.monkale.io 28 | resources: 29 | - dnszones/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /config/rbac/dnszone_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view dnszones. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: dnszone-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: coredns-manager-operator 10 | app.kubernetes.io/part-of: coredns-manager-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: dnszone-viewer-role 13 | rules: 14 | - apiGroups: 15 | - monkale.monkale.io 16 | resources: 17 | - dnszones 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - monkale.monkale.io 24 | resources: 25 | - dnszones/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | # Comment the following 4 lines if you want to disable 13 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 14 | # which protects your /metrics endpoint. 15 | - auth_proxy_service.yaml 16 | - auth_proxy_role.yaml 17 | - auth_proxy_role_binding.yaml 18 | - auth_proxy_client_clusterrole.yaml 19 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: role 7 | app.kubernetes.io/instance: leader-election-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: coredns-manager-operator 10 | app.kubernetes.io/part-of: coredns-manager-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: leader-election-role 13 | rules: 14 | - apiGroups: 15 | - "" 16 | resources: 17 | - configmaps 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - create 23 | - update 24 | - patch 25 | - delete 26 | - apiGroups: 27 | - coordination.k8s.io 28 | resources: 29 | - leases 30 | verbs: 31 | - get 32 | - list 33 | - watch 34 | - create 35 | - update 36 | - patch 37 | - delete 38 | - apiGroups: 39 | - "" 40 | resources: 41 | - events 42 | verbs: 43 | - create 44 | - patch 45 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: rolebinding 6 | app.kubernetes.io/instance: leader-election-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: coredns-manager-operator 9 | app.kubernetes.io/part-of: coredns-manager-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: leader-election-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: Role 15 | name: leader-election-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: manager-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - apps 21 | resources: 22 | - daemonsets 23 | verbs: 24 | - create 25 | - delete 26 | - get 27 | - list 28 | - patch 29 | - update 30 | - watch 31 | - apiGroups: 32 | - apps 33 | resources: 34 | - deployments 35 | verbs: 36 | - create 37 | - delete 38 | - get 39 | - list 40 | - patch 41 | - update 42 | - watch 43 | - apiGroups: 44 | - apps 45 | resources: 46 | - statefulsets 47 | verbs: 48 | - create 49 | - delete 50 | - get 51 | - list 52 | - patch 53 | - update 54 | - watch 55 | - apiGroups: 56 | - monkale.monkale.io 57 | resources: 58 | - dnsconnectors 59 | verbs: 60 | - create 61 | - delete 62 | - get 63 | - list 64 | - patch 65 | - update 66 | - watch 67 | - apiGroups: 68 | - monkale.monkale.io 69 | resources: 70 | - dnsconnectors/finalizers 71 | verbs: 72 | - update 73 | - apiGroups: 74 | - monkale.monkale.io 75 | resources: 76 | - dnsconnectors/status 77 | verbs: 78 | - get 79 | - patch 80 | - update 81 | - apiGroups: 82 | - monkale.monkale.io 83 | resources: 84 | - dnsrecords 85 | verbs: 86 | - create 87 | - delete 88 | - get 89 | - list 90 | - patch 91 | - update 92 | - watch 93 | - apiGroups: 94 | - monkale.monkale.io 95 | resources: 96 | - dnsrecords/finalizers 97 | verbs: 98 | - update 99 | - apiGroups: 100 | - monkale.monkale.io 101 | resources: 102 | - dnsrecords/status 103 | verbs: 104 | - get 105 | - patch 106 | - update 107 | - apiGroups: 108 | - monkale.monkale.io 109 | resources: 110 | - dnszones 111 | verbs: 112 | - create 113 | - delete 114 | - get 115 | - list 116 | - patch 117 | - update 118 | - watch 119 | - apiGroups: 120 | - monkale.monkale.io 121 | resources: 122 | - dnszones/finalizers 123 | verbs: 124 | - update 125 | - apiGroups: 126 | - monkale.monkale.io 127 | resources: 128 | - dnszones/status 129 | verbs: 130 | - get 131 | - patch 132 | - update 133 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: manager-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: coredns-manager-operator 9 | app.kubernetes.io/part-of: coredns-manager-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: manager-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: manager-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: serviceaccount 6 | app.kubernetes.io/instance: controller-manager-sa 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: coredns-manager-operator 9 | app.kubernetes.io/part-of: coredns-manager-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monkale_v1alpha1_dnszone.yaml 3 | - monkale_v1alpha1_dnsrecord.yaml 4 | - monkale_v1alpha1_dnsconnector.yaml 5 | #+kubebuilder:scaffold:manifestskustomizesamples -------------------------------------------------------------------------------- /config/samples/monkale_v1alpha1_dnsconnector.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: monkale.monkale.io/v1alpha1 2 | kind: DNSConnector 3 | metadata: 4 | name: coredns 5 | namespace: kube-system 6 | spec: 7 | waitForUpdateTimeout: 120 8 | corednsCM: 9 | name: coredns 10 | corefileKey: Corefile 11 | corednsDeployment: 12 | name: coredns 13 | type: Deployment 14 | corednsZoneEnaledPlugins: 15 | - errors 16 | - log -------------------------------------------------------------------------------- /config/samples/monkale_v1alpha1_dnsrecord.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: monkale.monkale.io/v1alpha1 3 | kind: DNSRecord 4 | metadata: 5 | name: www-a-market-example 6 | namespace: kube-system 7 | labels: 8 | app.kubernetes.io/name: dnsrecord 9 | app.kubernetes.io/instance: dnsrecord-sample 10 | app.kubernetes.io/part-of: coredns-manager-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | app.kubernetes.io/created-by: coredns-manager-operator 13 | spec: 14 | record: 15 | name: "www" 16 | value: "10.100.100.10" 17 | type: "A" 18 | dnsZoneRef: 19 | name: "market-example-zone" 20 | 21 | --- 22 | apiVersion: monkale.monkale.io/v1alpha1 23 | kind: DNSRecord 24 | metadata: 25 | name: app1-a-fqdn-test-market-example 26 | namespace: kube-system 27 | labels: 28 | app.kubernetes.io/name: dnsrecord 29 | app.kubernetes.io/instance: dnsrecord-sample 30 | app.kubernetes.io/part-of: coredns-manager-operator 31 | app.kubernetes.io/managed-by: kustomize 32 | app.kubernetes.io/created-by: coredns-manager-operator 33 | spec: 34 | record: 35 | name: "app1.market.example.com." 36 | value: "10.100.100.10" 37 | type: "A" 38 | dnsZoneRef: 39 | name: "market-example-zone" 40 | 41 | --- 42 | apiVersion: monkale.monkale.io/v1alpha1 43 | kind: DNSRecord 44 | metadata: 45 | name: mail-a-market-example 46 | namespace: kube-system 47 | labels: 48 | app.kubernetes.io/name: dnsrecord 49 | app.kubernetes.io/instance: dnsrecord-sample 50 | app.kubernetes.io/part-of: coredns-manager-operator 51 | app.kubernetes.io/managed-by: kustomize 52 | app.kubernetes.io/created-by: coredns-manager-operator 53 | spec: 54 | record: 55 | name: "mail" 56 | value: "10.100.100.20" 57 | type: "A" 58 | dnsZoneRef: 59 | name: "market-example-zone" 60 | 61 | --- 62 | apiVersion: monkale.monkale.io/v1alpha1 63 | kind: DNSRecord 64 | metadata: 65 | name: ns2-a-market-example 66 | namespace: kube-system 67 | labels: 68 | app.kubernetes.io/name: dnsrecord 69 | app.kubernetes.io/instance: dnsrecord-sample 70 | app.kubernetes.io/part-of: coredns-manager-operator 71 | app.kubernetes.io/managed-by: kustomize 72 | app.kubernetes.io/created-by: coredns-manager-operator 73 | spec: 74 | record: 75 | name: "ns2" 76 | value: "192.168.1.2" 77 | type: "A" 78 | dnsZoneRef: 79 | name: "market-example-zone" 80 | 81 | --- 82 | apiVersion: monkale.monkale.io/v1alpha1 83 | kind: DNSRecord 84 | metadata: 85 | name: ftp-cname-market-example 86 | namespace: kube-system 87 | labels: 88 | app.kubernetes.io/name: dnsrecord 89 | app.kubernetes.io/instance: dnsrecord-sample 90 | app.kubernetes.io/part-of: coredns-manager-operator 91 | app.kubernetes.io/managed-by: kustomize 92 | app.kubernetes.io/created-by: coredns-manager-operator 93 | spec: 94 | record: 95 | name: "ftp" 96 | value: "www.market.example.com." 97 | type: "CNAME" 98 | dnsZoneRef: 99 | name: "market-example-zone" 100 | 101 | --- 102 | apiVersion: monkale.monkale.io/v1alpha1 103 | kind: DNSRecord 104 | metadata: 105 | name: mail-mx-market-example 106 | namespace: kube-system 107 | labels: 108 | app.kubernetes.io/name: dnsrecord 109 | app.kubernetes.io/instance: dnsrecord-sample 110 | app.kubernetes.io/part-of: coredns-manager-operator 111 | app.kubernetes.io/managed-by: kustomize 112 | app.kubernetes.io/created-by: coredns-manager-operator 113 | spec: 114 | record: 115 | name: "@" 116 | value: "10 mail.market.example.com." 117 | type: "MX" 118 | dnsZoneRef: 119 | name: "market-example-zone" 120 | 121 | --- 122 | apiVersion: monkale.monkale.io/v1alpha1 123 | kind: DNSRecord 124 | metadata: 125 | name: spf-txt-market-example 126 | namespace: kube-system 127 | labels: 128 | app.kubernetes.io/name: dnsrecord 129 | app.kubernetes.io/instance: dnsrecord-sample 130 | app.kubernetes.io/part-of: coredns-manager-operator 131 | app.kubernetes.io/managed-by: kustomize 132 | app.kubernetes.io/created-by: coredns-manager-operator 133 | spec: 134 | record: 135 | name: "@" 136 | value: "v=spf1 include:example.com ~all" 137 | type: "TXT" 138 | dnsZoneRef: 139 | name: "market-example-zone" 140 | 141 | --- 142 | apiVersion: monkale.monkale.io/v1alpha1 143 | kind: DNSRecord 144 | metadata: 145 | name: ns1-aaaa-market-example 146 | namespace: kube-system 147 | labels: 148 | app.kubernetes.io/name: dnsrecord 149 | app.kubernetes.io/instance: dnsrecord-sample 150 | app.kubernetes.io/part-of: coredns-manager-operator 151 | app.kubernetes.io/managed-by: kustomize 152 | app.kubernetes.io/created-by: coredns-manager-operator 153 | spec: 154 | record: 155 | name: "ns1" 156 | value: "2001:db8::2" 157 | type: "AAAA" 158 | dnsZoneRef: 159 | name: "market-example-zone" 160 | 161 | --- 162 | apiVersion: monkale.monkale.io/v1alpha1 163 | kind: DNSRecord 164 | metadata: 165 | name: srv-market-example 166 | namespace: kube-system 167 | labels: 168 | app.kubernetes.io/name: dnsrecord 169 | app.kubernetes.io/instance: dnsrecord-sample 170 | app.kubernetes.io/part-of: coredns-manager-operator 171 | app.kubernetes.io/managed-by: kustomize 172 | app.kubernetes.io/created-by: coredns-manager-operator 173 | spec: 174 | record: 175 | name: "_sip._tcp" 176 | value: "10 5 5060 sipserver.example.com." 177 | type: "SRV" 178 | dnsZoneRef: 179 | name: "market-example-zone" 180 | 181 | --- 182 | apiVersion: monkale.monkale.io/v1alpha1 183 | kind: DNSRecord 184 | metadata: 185 | name: dnskey-market-example 186 | namespace: kube-system 187 | labels: 188 | app.kubernetes.io/name: dnsrecord 189 | app.kubernetes.io/instance: dnsrecord-sample 190 | app.kubernetes.io/part-of: coredns-manager-operator 191 | app.kubernetes.io/managed-by: kustomize 192 | app.kubernetes.io/created-by: coredns-manager-operator 193 | spec: 194 | record: 195 | name: "@" 196 | value: "257 3 8 AwEAAaKdjGVkvFjdLv...5A5t5y7yQ8r" 197 | type: "DNSKEY" 198 | dnsZoneRef: 199 | name: "market-example-zone" 200 | 201 | --- 202 | apiVersion: monkale.monkale.io/v1alpha1 203 | kind: DNSRecord 204 | metadata: 205 | name: rrsig-market-example 206 | namespace: kube-system 207 | labels: 208 | app.kubernetes.io/name: dnsrecord 209 | app.kubernetes.io/instance: dnsrecord-sample 210 | app.kubernetes.io/part-of: coredns-manager-operator 211 | app.kubernetes.io/managed-by: kustomize 212 | app.kubernetes.io/created-by: coredns-manager-operator 213 | spec: 214 | record: 215 | name: "@" 216 | value: "A 8 3 86400 20240601000000 20240501000000 12345 market.example.com. b1N2...==" 217 | type: "RRSIG" 218 | dnsZoneRef: 219 | name: "market-example-zone" 220 | 221 | --- 222 | apiVersion: monkale.monkale.io/v1alpha1 223 | kind: DNSRecord 224 | metadata: 225 | name: hinfo-market-example 226 | namespace: kube-system 227 | labels: 228 | app.kubernetes.io/name: dnsrecord 229 | app.kubernetes.io/instance: dnsrecord-sample 230 | app.kubernetes.io/part-of: coredns-manager-operator 231 | app.kubernetes.io/managed-by: kustomize 232 | app.kubernetes.io/created-by: coredns-manager-operator 233 | spec: 234 | record: 235 | name: "@" 236 | value: "Intel x86_64" 237 | type: "HINFO" 238 | dnsZoneRef: 239 | name: "market-example-zone" 240 | 241 | --- 242 | apiVersion: monkale.monkale.io/v1alpha1 243 | kind: DNSRecord 244 | metadata: 245 | name: record-no-zone-test 246 | namespace: kube-system 247 | spec: 248 | record: 249 | name: "haproxy.cpe" 250 | value: "10.149.149.10" 251 | type: "A" 252 | dnsZoneRef: 253 | name: "this-zone-does-not-exist" -------------------------------------------------------------------------------- /config/samples/monkale_v1alpha1_dnszone.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: monkale.monkale.io/v1alpha1 3 | kind: DNSZone 4 | metadata: 5 | name: market-example-zone 6 | namespace: kube-system 7 | spec: 8 | connectorName: coredns 9 | domain: "market.example.com" 10 | primaryNS: 11 | ipAddress: "10.100.100.254" 12 | respPersonEmail: "admin@example.com" 13 | 14 | --- 15 | apiVersion: monkale.monkale.io/v1alpha1 16 | kind: DNSZone 17 | metadata: 18 | name: no-connector-test-zone 19 | namespace: kube-system 20 | spec: 21 | connectorName: this-connector-does-not-exist 22 | domain: "test.com." 23 | primaryNS: 24 | ipAddress: "10.120.100.11" 25 | respPersonEmail: "admin@test.local" -------------------------------------------------------------------------------- /config/scorecard/bases/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: scorecard.operatorframework.io/v1alpha3 2 | kind: Configuration 3 | metadata: 4 | name: config 5 | stages: 6 | - parallel: true 7 | tests: [] 8 | -------------------------------------------------------------------------------- /config/scorecard/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - bases/config.yaml 3 | patchesJson6902: 4 | - path: patches/basic.config.yaml 5 | target: 6 | group: scorecard.operatorframework.io 7 | version: v1alpha3 8 | kind: Configuration 9 | name: config 10 | - path: patches/olm.config.yaml 11 | target: 12 | group: scorecard.operatorframework.io 13 | version: v1alpha3 14 | kind: Configuration 15 | name: config 16 | #+kubebuilder:scaffold:patchesJson6902 17 | -------------------------------------------------------------------------------- /config/scorecard/patches/basic.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - basic-check-spec 7 | image: quay.io/operator-framework/scorecard-test:v1.33.0 8 | labels: 9 | suite: basic 10 | test: basic-check-spec-test 11 | -------------------------------------------------------------------------------- /config/scorecard/patches/olm.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - olm-bundle-validation 7 | image: quay.io/operator-framework/scorecard-test:v1.33.0 8 | labels: 9 | suite: olm 10 | test: olm-bundle-validation-test 11 | - op: add 12 | path: /stages/0/tests/- 13 | value: 14 | entrypoint: 15 | - scorecard-test 16 | - olm-crds-have-validation 17 | image: quay.io/operator-framework/scorecard-test:v1.33.0 18 | labels: 19 | suite: olm 20 | test: olm-crds-have-validation-test 21 | - op: add 22 | path: /stages/0/tests/- 23 | value: 24 | entrypoint: 25 | - scorecard-test 26 | - olm-crds-have-resources 27 | image: quay.io/operator-framework/scorecard-test:v1.33.0 28 | labels: 29 | suite: olm 30 | test: olm-crds-have-resources-test 31 | - op: add 32 | path: /stages/0/tests/- 33 | value: 34 | entrypoint: 35 | - scorecard-test 36 | - olm-spec-descriptors 37 | image: quay.io/operator-framework/scorecard-test:v1.33.0 38 | labels: 39 | suite: olm 40 | test: olm-spec-descriptors-test 41 | - op: add 42 | path: /stages/0/tests/- 43 | value: 44 | entrypoint: 45 | - scorecard-test 46 | - olm-status-descriptors 47 | image: quay.io/operator-framework/scorecard-test:v1.33.0 48 | labels: 49 | suite: olm 50 | test: olm-status-descriptors-test 51 | -------------------------------------------------------------------------------- /deploy/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monkale-io/coredns-manager-operator/29157f89f232d0106a2551ef2a901b9c8009f7b2/deploy/.gitkeep -------------------------------------------------------------------------------- /docs/coredns_exposure.md: -------------------------------------------------------------------------------- 1 | # Exposing CoreDNS 2 | The CoreDNS Manager Operator is designed to manage CoreDNS zones, but it does not expose CoreDNS to the network by default. Administrators must handle exposure themselves using available methods according to their security requirements. This can include ingress rules, external load balancers, or other methods. 3 | 4 | ## Exposing master nodes approach 5 | This document covers scenarios where master nodes are used as DNS servers. Administrators will need to set up all nodes accordingly. 6 | 7 | ### HostPort Method 8 | One common way to expose CoreDNS is by using hostPort. This method involves scaling CoreDNS pods to match the number of Kubernetes master nodes, exposing port 53, and configuring a rolling update strategy to replace pods during updates. This approach has minimal downtime during CoreDNS updates, typically just a few seconds, and is reliable during master node failures. 9 | 10 | For example, with three master nodes, you can use the following commands: 11 | ```sh 12 | kubectl scale deployment coredns --replicas 0 -n kube-system 13 | kubectl patch deployment coredns -n kube-system --type='json' -p='[ 14 | { 15 | "op": "add", 16 | "path": "/spec/template/spec/containers/0/ports/0/hostPort", 17 | "value": 53 18 | }, 19 | { 20 | "op": "add", 21 | "path": "/spec/template/spec/containers/0/ports/1/hostPort", 22 | "value": 53 23 | } 24 | ]' 25 | kubectl patch deployment coredns -n kube-system --type='json' -p='[{"op": "replace", "path": "/spec/strategy", "value": {"type": "RollingUpdate","rollingUpdate": {"maxUnavailable": 1,"maxSurge": 1}}}]' 26 | kubectl scale deployment coredns --replicas 3 -n kube-system 27 | ``` 28 | 29 | For a practical example, refer to this guide: [Installing Monkale CoreDNS Manager Operator on Single Node Talos](https://medium.com/@nicholas5421/installing-monkale-coredns-manager-operator-on-single-node-talos-16f8be900585) 30 | 31 | 32 | ### K3s Klipper LB Method 33 | 34 | K3s has a built-in Klipper Load Balancer, which simplifies exposure. For K3s users, you can patch the kube-dns service spec type to `LoadBalancer`. 35 | 36 | ```sh 37 | kubectl patch service kube-dns --type='merge' --namespace kube-system \ 38 | -p='{"spec":{"type": "LoadBalancer"}}' 39 | ``` 40 | 41 | Using this approach, it is recommended to add more replicas for each node to ensure zone updates occur without disruption. 42 | 43 | **Note:** If you have a multi-node setup and use this method, keep in mind that name resolution could be disrupted for up to 2 minutes in the event of a master node failure. The secondary DNS resolution process and reconciliation could take this long. 44 | 45 | For a detailed guide, refer to this article: [Managing Internal DNS in Air-Gapped K3s Clusters with Monkale CoreDNS Manager Operator](https://medium.com/@nicholas5421/managing-internal-dns-in-air-gapped-k3s-clusters-with-monkale-coredns-manager-operator-fa1c9136cc2c) 46 | -------------------------------------------------------------------------------- /docs/develop.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | * You’ll need a Kubernetes to run against. 4 | * kube-dns service is exposed 5 | * It is recommended to switch the current context to the kube-system 6 | 7 | ### Go 1.20 8 | The project is done using go1.20. 9 | 10 | Install. 11 | ```sh 12 | # install go.120 13 | go install golang.org/dl/go1.20@latest 14 | GOPATH=`go env GOPATH` 15 | $GOPATH/bin/go1.20 download 16 | 17 | # export 18 | export GOROOT=`$(go env GOPATH)/bin/go1.20 env GOROOT` 19 | export PATH=${GOROOT}/bin:${PATH} 20 | 21 | # check version 22 | go version 23 | ``` 24 | 25 | 26 | 27 | ### Running on the cluster 28 | 1. Install Instances of Custom Resources: 29 | 30 | ```sh 31 | kubectl apply -k config/samples/ 32 | ``` 33 | 34 | 2. Build and push your image to the location specified by `IMG`: 35 | 36 | ```sh 37 | make docker-build docker-push IMG=/coredns-manager-operator:tag 38 | ``` 39 | 40 | 3. Update namespace if needed in `config/default/kustomization.yaml`. Operator shall be installed in the same namespace as your coredns. 41 | 42 | 4. Deploy the controller to the cluster with the image specified by `IMG`: 43 | 44 | ```sh 45 | make deploy IMG=/coredns-manager-operator:tag 46 | ``` 47 | OR, Alternatively, generate manifests using kustomize. 48 | ```sh 49 | make installation-manifests IMG=/coredns-manager-operator:tag 50 | kubectl apply -f deploy/operator.yaml 51 | ``` 52 | 53 | 54 | ### Uninstall CRDs 55 | To delete the CRDs from the cluster: 56 | 57 | ```sh 58 | make uninstall 59 | ``` 60 | 61 | ### Undeploy controller 62 | UnDeploy the controller from the cluster: 63 | 64 | ```sh 65 | make undeploy 66 | ``` 67 | 68 | ### How it works 69 | This project aims to follow the Kubernetes [Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/). 70 | 71 | It uses [Controllers](https://kubernetes.io/docs/concepts/architecture/controller/), 72 | which provide a reconcile function responsible for synchronizing resources until the desired state is reached on the cluster. 73 | 74 | ### Test It Out 75 | 1. Install the CRDs into the cluster: 76 | ```sh 77 | make install 78 | ``` 79 | 80 | 2. Run your controller (this will run in the foreground, so switch to a new terminal if you want to leave it running): 81 | ```sh 82 | make run 83 | ``` 84 | 85 | **NOTE:** You can also run this in one step by running: `make install run` 86 | 87 | **NOTE:** You can also run `make from-scratch` to run all together: recreate kind cluster, set kube-system as current context, generate, manifests, install and run. 88 | 89 | #### Manual test 90 | Since the project does not have unit tests yet, it is recommended to follow the manual QA guide to ensure the quality of the controller. 91 | [Manual QA Guide](qa/dev-manual-qa-guide.md) 92 | 93 | ### Modifying the API definitions 94 | If you are editing the API definitions, generate the manifests such as CRs or CRDs using: 95 | 96 | ```sh 97 | make manifests 98 | ``` 99 | 100 | **NOTE:** Run `make --help` for more information on all potential `make` targets 101 | 102 | ## License 103 | 104 | Copyright 2024 monkale.io. 105 | 106 | Licensed under the Apache License, Version 2.0 (the "License"); 107 | you may not use this file except in compliance with the License. 108 | You may obtain a copy of the License at 109 | 110 | http://www.apache.org/licenses/LICENSE-2.0 111 | 112 | Unless required by applicable law or agreed to in writing, software 113 | distributed under the License is distributed on an "AS IS" BASIS, 114 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 115 | See the License for the specific language governing permissions and 116 | limitations under the License. -------------------------------------------------------------------------------- /docs/dnsconnector.md: -------------------------------------------------------------------------------- 1 | # DNSConnector Resource Documentation 2 | 3 | ## Overview 4 | 5 | The `DNSConnector` resource defines the connection between DNS zones and the CoreDNS deployment within the Kubernetes cluster. This resource allows you to specify the details of the CoreDNS configuration, deployment type, and additional settings required for the DNSConnector to function. 6 | 7 | ## Specifying a DNSConnector 8 | 9 | ### Schema 10 | The schema for the DNSConnector resource is as follows: 11 | 12 | ```yaml 13 | apiVersion: monkale.monkale.io/v1alpha1 14 | kind: DNSConnector 15 | metadata: 16 | name: coredns 17 | namespace: kube-system 18 | spec: 19 | waitForUpdateTimeout: 300 20 | corednsCM: 21 | name: "coredns" 22 | corefileKey: "Corefile" 23 | corednsDeployment: 24 | type: "Deployment" 25 | name: "coredns" 26 | zonefilesMountDir: "/opt/coredns" 27 | corednsZoneEnaledPlugins: 28 | - "errors" 29 | - "log" 30 | ``` 31 | 32 | ### Fields 33 | 34 | #### spec.waitForUpdateTimeout 35 | * `waitForUpdateTimeout` (int, optional): Specifies how long the DNSConnector should wait for CoreDNS to complete the update. If CoreDNS deployment hasn't completed the update within this time, the controller will perform a rollback. The default value is 120 seconds (2 minutes). 36 | 37 | #### spec.corednsCM 38 | * `corednsCM` (object, required): The name and corefile key of the CoreDNS ConfigMap. 39 | * `name` (string, optional): The name of the CoreDNS ConfigMap that contains the Corefile. Default is coredns. 40 | * `corefileKey` (string, optional): The key whose value is the Corefile. Default is Corefile. 41 | 42 | #### spec.corednsDeployment 43 | * `corednsDeployment` (object, required): Specifies the CoreDNS deployment type and name. 44 | * `type` (string, required): The type of the CoreDNS resource (e.g., Deployment, StatefulSet, DaemonSet). 45 | * `name` (string, optional): The name of the CoreDNS resource. This field is optional if type is Pod and a LabelSelector is specified. 46 | * `zonefilesMountDir` (string, optional): Specifies the mount path for zone files. Default is /opt/coredns. 47 | 48 | #### spec.corednsZoneEnaledPlugins 49 | `corednsZoneEnaledPlugins` (array of strings, optional): List of enabled CoreDNS plugins. Refer to the CoreDNS plugins documentation for more details. Common plugins include errors and log. 50 | Example Resources 51 | 52 | ### Examples 53 | 54 | #### For most situations 55 | 56 | In most popular Kubernetes distributions, these settings will remain the same, so you can use the following template without modifications. 57 | 58 | ```yaml 59 | apiVersion: monkale.monkale.io/v1alpha1 60 | kind: DNSConnector 61 | metadata: 62 | name: coredns 63 | spec: 64 | corednsCM: 65 | name: "coredns" 66 | corefileKey: "Corefile" 67 | corednsDeployment: 68 | type: "Deployment" 69 | name: "coredns" 70 | ``` 71 | 72 | #### Adjusted coredns example 73 | 74 | In this example we have changed the cm mount path to /var/lib/coredns, and corednsDeployment set to DaemonSet. Additionally, "errors" and "log" plugins have been activated. 75 | 76 | ```yaml 77 | apiVersion: monkale.monkale.io/v1alpha1 78 | kind: DNSConnector 79 | metadata: 80 | name: connector 81 | spec: 82 | corednsCM: 83 | name: "coredns" 84 | corefileKey: "Corefile" 85 | corednsDeployment: 86 | type: "DaemonSet" 87 | name: "coredns" 88 | zonefilesMountDir: "/var/lib/coredns" 89 | corednsZoneEnaledPlugins: 90 | - "errors" 91 | - "log" 92 | ``` 93 | 94 | #### Unplugging the DNSConnector 95 | 96 | If you decide to stop using the coredns-manager-operator, ensure proper unplugging of the DNSConnector to avoid issues. 97 | 98 | 1. Remove all associated DNSZones. 99 | ```sh 100 | $ kubectl delete dnsconnectors coredns -n kube-system 101 | ``` 102 | 103 | 2. Remove connector. 104 | ```sh 105 | $ kubectl delete dnsconnectors coredns -n kube-system 106 | ``` 107 | 108 | 3. The original coredns configMap has been restored. 109 | ```sh 110 | $ kubectl describe cm coredns 111 | ``` 112 | 113 | ## Status 114 | The DNSConnector resource also includes status fields that reflect the observed state of the resource. 115 | 116 | ### Status Fields 117 | * `conditions` (array): Indicates the status of the DNSConnector. Each condition includes: 118 | * `provisionedZones` (array): Displays DNSZones and their versions currently provisioned to CoreDNS. 119 | 120 | 121 | ### States 122 | `conditions[].reason` represents DNSZone state. 123 | 124 | * `Active` - The DNSConnector and coredns are up-to-date with the latest changes. 125 | * `Updating` - The DNSConnector is currently updating the CoreDNS deployment. If the DNSConnector gets stuck in the `Updating` state, it might indicate an issue during reconciliation. Check operator's logs for more information. 126 | * `UpdateErr` - DNSConnector failure. Describe the resource and check logs. Name resolution might be impacted. 127 | 128 | ### Example Status 129 | 130 | ```json 131 | { 132 | "conditions": [ 133 | { 134 | "lastTransitionTime": "2024-06-04T02:25:59Z", 135 | "message": "CoreDNS Ready", 136 | "observedGeneration": 2, 137 | "reason": "Active", 138 | "status": "True", 139 | "type": "Ready" 140 | } 141 | ], 142 | "provisionedZones": [ 143 | { 144 | "domain": "custom.com.", 145 | "name": "custom-dnszone", 146 | "serialNumber": "0603210914" 147 | } 148 | ] 149 | } 150 | ``` 151 | 152 | ## Troubleshoot 153 | 154 | * If `kubectl get dnsconnectors` indicates any misconfiguration, it requires prompt attention. 155 | 156 | * DNSConnector issues commonly arise during initial implementation of the coredns-manager-operator or due to incorrect patching procedures. 157 | 158 | * It's crucial to recognize that certain types of misconfigurations could potentially cause the operator to crash. 159 | 160 | ### DNSConnector misconfigured example 161 | 162 | In the provided example, the `spec.corednsCM.corefileKey` points to the wrong location, resulting in an error when trying to detect the Corefile. To resolve this issue, follow these steps to identify the correct keyfile and patch the DNSConnector: 163 | 164 | ```sh 165 | kubectl get dnsconnectors 166 | NAME LAST CHANGE STATE MESSAGE 167 | coredns 2024-06-03T16:09:19Z Error could not detect corefile: key RandomString not found in CoreDNS ConfigMap 168 | ``` 169 | 170 | To overcome, find the correct keyfile and patch the DNSConnector's `spec.corednsCM.corefileKey`. 171 | ```sh 172 | # Identify the correct keyfile location 173 | $ kubectl get configmap coredns -oyaml 174 | apiVersion: v1 175 | data: 176 | Corefile: | 177 | ... 178 | 179 | # Patch DNSConnector 180 | $ kubectl patch DNSConnector coredns --type='merge' -p='{"spec":{"corednsCM": {"corefileKey": "Corefile"}}}' 181 | ``` 182 | 183 | ### Troubleshooting DNSConnector stucked with the message "coredns is being updated" or "healthcheck failure: coredns is not healthy. Check coredns deployment log". 184 | 185 | These message may be observed when users have set incorrect values in the `spec.corednsZonePlugins`. Visit [Coredns - plugins](https://coredns.io/plugins) for more info. 186 | Double-checking and ensuring that the specified plugins are correctly configured can help resolve this issue. 187 | 188 | Check coredns logs 189 | ```sh 190 | $ kubectl logs -l 'k8s-app=kube-dns' 191 | [ERROR] Restart failed: /etc/coredns/Corefile:51 - Error during parsing: Unknown directive 'banana' 192 | [ERROR] plugin/reload: Corefile changed but reload failed: starting with listener file descriptors: /etc/coredns/Corefile:51 - Error during parsing: Unknown directive 'banana' 193 | /etc/coredns/Corefile:52 - Error during parsing: Unknown directive 'banana' 194 | /etc/coredns/Corefile:52 - Error during parsing: Unknown directive 'banana' 195 | ``` 196 | 197 | To fix it, remove `banana` from `spec.corednsZonePlugins`. 198 | 199 | ### DNSConnector no status 200 | 201 | If `kubectl get dnsconnectors` isn't displaying any status for your connector, check the `controller-manager-operator` logs for potential RBAC issues and report any findings. -------------------------------------------------------------------------------- /docs/dnsrecords.md: -------------------------------------------------------------------------------- 1 | # DNSRecord Resource Documentation 2 | 3 | ## Overview 4 | 5 | The `DNSRecord` resource defines DNS records within the Kubernetes cluster. This resource allows you to specify the details of a DNS record, such as its name, value, type, TTL (Time To Live), and the DNS zone it belongs to. 6 | 7 | ## Specifying a DNSRecord 8 | 9 | ### Schema 10 | 11 | The schema for the DNSRecord resource is as follows: 12 | 13 | ```yaml 14 | apiVersion: monkale.monkale.io/v1alpha1 15 | kind: DNSRecord 16 | metadata: 17 | name: example-dnsrecord 18 | namespace: kube-system 19 | spec: 20 | record: 21 | name: "www.example.com." 22 | value: "192.0.2.1" 23 | type: "A" 24 | ttl: "300" 25 | dnsZoneRef: 26 | name: example-dnszone 27 | namespace: default 28 | ``` 29 | 30 | ### Fields 31 | 32 | #### spec.record 33 | 34 | * `name` (string): The name of the DNS record, e.g., a domain name for an A record. 35 | * `value` (string): The value of the DNS record, e.g., an IP address for an A record. 36 | * `type` (string): The type of the DNS record according to RFC1035. Supported types are A, AAAA, CNAME, MX, TXT, NS, PTR, SRV, CAA, DNSKEY, DS, NAPTR, RRSIG, DNAME, and HINFO. 37 | * `ttl` (string, optional): The Time To Live (TTL) for the DNS record. If not set, the default is the minimum TTL value in the SOA record. For simple scenarios it suggest to leave the default ttl. 38 | 39 | #### spec.dnsZoneRef 40 | 41 | * `name` (string): The name of the DNSZone instance to which this record will publish its endpoints. 42 | 43 | ### Examples 44 | 45 | #### A Record 46 | 47 | ```yaml 48 | apiVersion: monkale.monkale.io/v1alpha1 49 | kind: DNSRecord 50 | metadata: 51 | name: a-record-example 52 | namespace: kube-system 53 | spec: 54 | record: 55 | name: "www.example.com." 56 | value: "192.0.2.1" 57 | type: "A" 58 | dnsZoneRef: 59 | name: example-dnszone 60 | namespace: default 61 | ``` 62 | 63 | #### CNAME Record 64 | 65 | ```yaml 66 | apiVersion: monkale.monkale.io/v1alpha1 67 | kind: DNSRecord 68 | metadata: 69 | name: cname-record-example 70 | namespace: kube-system 71 | spec: 72 | record: 73 | name: "mail.example.com." 74 | value: "www.example.com." 75 | type: "CNAME" 76 | dnsZoneRef: 77 | name: example-dnszone 78 | namespace: default 79 | ``` 80 | 81 | #### MX Record with TTL 82 | 83 | ```yaml 84 | apiVersion: monkale.monkale.io/v1alpha1 85 | kind: DNSRecord 86 | metadata: 87 | name: mx-record-example 88 | namespace: kube-system 89 | spec: 90 | record: 91 | name: "example.com." 92 | value: "10 mail.example.com." 93 | type: "MX" 94 | ttl: "1800" 95 | dnsZoneRef: 96 | name: example-dnszone 97 | namespace: default 98 | ``` 99 | 100 | #### More examples 101 | For more examples visit 102 | [DNSRecord Samples](../config/samples/monkale_v1alpha1_dnsrecord.yaml) 103 | 104 | ## Status 105 | The DNSRecord resource also includes status fields that reflect the observed state of the resource. 106 | 107 | ### Status Fields 108 | * `conditions` (array): Indicates the status of the DNSRecord. Each 109 | * `validationPassed` (boolean): Displays whether the record passed the syntax validation check. 110 | * `generatedRecord` (string): Displays the generated DNS record. 111 | 112 | ### States 113 | `conditions[].reason` represents DNSRecord state. 114 | 115 | * `Ready` - The DNSRecord has passed the syntax validation check and has been added the DNSZone' zonefile. 116 | * `Degraded` - The DNSRecord failed the syntax validation check. `Degraded` records are unresovable. 117 | * `Pending` - The DNSRecord has been created and passed the syntax validation check. It is waiting to be picked up by the DNSZone controller. 118 | 119 | ### Example Status 120 | 121 | ```json 122 | { 123 | "conditions": [ 124 | { 125 | "lastTransitionTime": "2024-06-03T16:33:18Z", 126 | "message": "CoreDNS Ready", 127 | "observedGeneration": 3, 128 | "reason": "Active", 129 | "status": "True", 130 | "type": "Ready" 131 | } 132 | ], 133 | "provisionedZones": [ 134 | { 135 | "domain": "market.example.com", 136 | "name": "market-example-zone", 137 | "serialNumber": "0603113254" 138 | } 139 | ] 140 | } 141 | ``` 142 | 143 | ## How does it work 144 | When a DNSRecord resource is created, its specifications are converted into zone file entries using the following template: 145 | ```go 146 | {{ .Spec.Record.Name }} {{ .Spec.Record.TTL }} IN {{ .Spec.Record.Type }} {{ .Spec.Record.Value -}} 147 | ``` 148 | This template generates zone file entries that conform to the RFC1035 format, ensuring compatibility with standard DNS servers like CoreDNS. 149 | 150 | ### Pay Attention to Domain Names and FQDNs 151 | Proper configuration of domain names and Fully Qualified Domain Names (FQDNs) is essential for accurate DNS resolution. An FQDN specifies the entire domain path, ending with a trailing dot (e.g., `www.example.com.`), which indicates the root of the DNS, because this format is typical for RFC-compliant zone files. 152 | 153 | #### Common Issues 154 | 1. FQDN vs. Relative Domain Names: 155 | * FQDN (with trailing dot): Treated as an absolute domain name (e.g., `some.external.domain.com.`). 156 | * Relative Domain Name (without trailing dot): Appended with the zone file origin (e.g., `some.external.domain.com` could be interpreted as `some.external.domain.com.`). 157 | * `www.` will be treated as an FQDN and may cause a resolution error. 158 | 2. Configuration Errors: 159 | * Without a trailing dot, a record like `www.market.example.com` might be interpreted as `www.market.example.com.market.example.com.` 160 | 161 | #### Solutions 162 | 1. Set Record as FQDN: 163 | Add a trailing dot to the record (e.g., www.market.example.com.) to indicate it as an FQDN. 164 | 165 | 2. Remove Domain Name: 166 | Use only the necessary part of the domain (e.g., www) to avoid misinterpretation. 167 | 168 | ## Troubleshoot 169 | 170 | ### DNSRecord in Degraded state 171 | 172 | When a DNS Record is in a degraded state, it typically indicates a syntax check issue. Bad syntax should be treated the same as if you were manually trying to add this record to the zone file. 173 | 174 | 175 | For example, in this record, there are two dots after `www`, which is obviously a bad fully qualified domain name (FQDN). 176 | 177 | ```sh 178 | $ kubectl describe dnsrecords bad-domain-record-test 179 | Name: bad-domain-record-test 180 | Namespace: kube-system 181 | Labels: 182 | Annotations: 183 | API Version: monkale.monkale.io/v1alpha1 184 | Kind: DNSRecord 185 | Metadata: 186 | Creation Timestamp: 2024-06-02T20:02:51Z 187 | Finalizers: 188 | dnsrecords/finalizers 189 | Generation: 3 190 | Resource Version: 34517 191 | UID: 6e5cc436-7fac-444b-b222-a854c86a96dc 192 | Spec: 193 | Dns Zone Ref: 194 | Name: market-example-zone 195 | Record: 196 | Name: www.. 197 | Type: A 198 | Value: 192.0.2.1 199 | Status: 200 | Conditions: 201 | Last Transition Time: 2024-06-03T14:24:25Z 202 | Message: Record validation failure: error parsing records: dns: bad owner name: "www.." at line: 1:6 203 | Observed Generation: 3 204 | Reason: Degraded 205 | Status: False 206 | Type: Ready 207 | Generated Record: www.. IN A 192.0.2.1 208 | Events: 209 | ``` 210 | 211 | 212 | ### DNSRecord in Pending state 213 | When a DNS Record is in a pending state, it means that the record has passed the validation process and is waiting to be picked up by the DNSZone controller. This state typically occurs when the record is newly created or updated and may only last for a few seconds before transitioning to an Ready state. 214 | 215 | To troubleshoot a DNSRecord in a pending state, you should first describe the resource and check the message field for any relevant information. Ensure that the `spec.dnsZoneRef` points to a valid DNSZone that exists in the cluster. If the DNSZone exists, proceed to describe the related `DNSZone` to further investigate any potential issues. 216 | 217 | ```sh 218 | $ kubectl describe dnsrecord record-no-zone-test 219 | Name: record-no-zone-test 220 | Namespace: kube-system 221 | Labels: 222 | Annotations: 223 | API Version: monkale.monkale.io/v1alpha1 224 | Kind: DNSRecord 225 | Metadata: 226 | Creation Timestamp: 2024-06-02T18:44:59Z 227 | Finalizers: 228 | dnsrecords/finalizers 229 | Generation: 1 230 | Resource Version: 592 231 | UID: 5334c0b1-fdc6-41c8-8efd-5281b95248b2 232 | Spec: 233 | Dns Zone Ref: 234 | Name: this-zone-does-not-exist 235 | Record: 236 | Name: haproxy.cpe 237 | Type: A 238 | Value: 10.149.149.10 239 | Status: 240 | Conditions: 241 | Last Transition Time: 2024-06-02T18:44:59Z 242 | Message: Record has been constructed. Awaiting for the dnszone controller to pick up the record 243 | Observed Generation: 1 244 | Reason: Pending 245 | Status: False 246 | Type: Ready 247 | Generated Record: haproxy.cpe IN A 10.149.149.10 248 | Validation Passed: true 249 | Events: 250 | ``` -------------------------------------------------------------------------------- /docs/dnszones.md: -------------------------------------------------------------------------------- 1 | # DNSZone Resource Documentation 2 | 3 | ## Overview 4 | 5 | The `DNSZone` resource defines DNS zones within the Kubernetes cluster. This resource allows you to specify the details of a DNS zone, such as its domain, primary name server, responsible person's email, and other configuration parameters. Essentually, DNSZone specifications are used to template the SOA record and primary NS record for the zone. 6 | 7 | ## Specifying a DNSZone 8 | 9 | ### Schema 10 | 11 | The schema for the DNSZone resource is as follows: 12 | 13 | ```yaml 14 | apiVersion: monkale.monkale.io/v1alpha1 15 | kind: DNSZone 16 | metadata: 17 | name: example-dnszone 18 | spec: 19 | cmPrefix: "coredns-zone-" 20 | domain: "example.com" 21 | primaryNS: 22 | hostname: "ns1" 23 | ipAddress: "192.0.2.2" 24 | recordType: "A" 25 | respPersonEmail: "admin@example.com" 26 | ttl: 86400 27 | refreshRate: 7200 28 | retryInterval: 3600 29 | expireTime: 1209600 30 | minimumTTL: 86400 31 | connectorName: "example-dnsconnector" 32 | ``` 33 | 34 | ### Fields 35 | 36 | #### spec.cmPrefix 37 | * `cmPrefix` (string, optional): Specifies the prefix for the zone file configmap. The default value is coredns-zone-. The CM Name format is "prefix" + "metadata.name". 38 | 39 | #### spec.domain 40 | * `domain` (string, required): Specifies the domain in which DNS records are valid. 41 | 42 | #### spec.primaryNS 43 | * `primaryNS` (object, required): Defines the primary nameserver for the zone, including: 44 | * `hostname` (string): The server name of the primary nameserver. Default is ns1. 45 | * `ipAddress` (string): The IP address of the DNS server where the zone is hosted. It should be the address of your kubernetes/load balancer. 46 | * `recordType` (string): The type of the record to be created for the NS's A record. Default is A. 47 | 48 | #### spec.respPersonEmail 49 | * `respPersonEmail` (string, required): The responsible party's email for the domain, typically formatted as admin@example.com but represented with a dot (.) instead of an at (@) in DNS records. 50 | 51 | #### spec.ttl 52 | * `ttl` (uint, optional): Specifies the default Time to Live (TTL) for the zone's records, indicating how long these records should be cached by DNS resolvers. Default is 86400 seconds (24 hours). 53 | 54 | #### spec.refreshRate 55 | * `refreshRate` (uint, optional): Defines the time a secondary DNS server waits before querying the primary DNS server to check for updates. Default is 7200 seconds (2 hours). 56 | 57 | #### spec.retryInterval 58 | * `retryInterval` (uint, optional): Defines how long a secondary server should wait before trying again to reconnect to the primary server after a failure. Default is 3600 seconds (1 hour). 59 | 60 | #### spec.expireTime 61 | * `expireTime` (uint, optional): Defines how long the secondary server should wait before discarding the zone data if it cannot reach the primary server. Default is 1209600 seconds (2 weeks). 62 | spec.minimumTTL 63 | 64 | #### minimumTTL 65 | * `minimumTTL` (uint, optional): Specifies the minimum amount of time that should be allowed for caching the DNS records. If individual records do not specify a TTL, this value should be used. Default is 86400 seconds (24 hours). 66 | 67 | #### spec.connectorName 68 | * `connectorName` (string, required): The name of the DNSConnector resource to which this zone will be linked. 69 | 70 | ### Examples 71 | 72 | #### Basic DNSZone (recommended for most users) 73 | ```yaml 74 | apiVersion: monkale.monkale.io/v1alpha1 75 | kind: DNSZone 76 | metadata: 77 | name: basic-dnszone 78 | spec: 79 | domain: "basic.com." 80 | primaryNS: 81 | hostname: "ns1" 82 | ipAddress: "192.0.2.2" 83 | recordType: "A" 84 | respPersonEmail: "admin@basic.com" 85 | connectorName: "coredns" 86 | ``` 87 | 88 | #### Advanced DNSZone with tuned SOA 89 | ```yaml 90 | apiVersion: monkale.monkale.io/v1alpha1 91 | kind: DNSZone 92 | metadata: 93 | name: custom-dnszone 94 | spec: 95 | domain: "custom.com." 96 | primaryNS: 97 | hostname: "nameserver1" 98 | ipAddress: "192.0.2.3" 99 | recordType: "A" 100 | respPersonEmail: "support@custom.com" 101 | ttl: 7200 102 | refreshRate: 3600 103 | retryInterval: 1800 104 | expireTime: 604800 105 | minimumTTL: 3600 106 | connectorName: "coredns" 107 | ``` 108 | 109 | ## Status 110 | The DNSZone resource also includes status fields that reflect the observed state of the resource. 111 | 112 | ### Status Fields 113 | * `conditions` (array): Indicates the status of the DNSZone. Each condition includes: 114 | * `currentZoneSerial` (string): The current version number of the zone file, implemented as time now formatted. Used to track the Zone version. 115 | 116 | * `recordCount` (int): The number of records in the zone, excluding SOA and primary ns records. 117 | 118 | * `validationPassed` (boolean): Displays whether the zone file passed the syntax validation check. 119 | 120 | * `zoneConfigmap` (string): The name of the generated zone config map. 121 | 122 | * `checkpoint` (bool): Indicates whether the DNSZone was previously active. This flag is used to instruct the DNSConnector to preserve the old version of the DNSZone in case the update process encounters an issue. 123 | 124 | ### States 125 | `conditions[].reason` represents DNSZone state. 126 | 127 | * `Active` - The DNSZone has passed the syntax validation check and has been picked up by the DNSConnector controller. 128 | * `UpdateErr` - An error occurred during the zone file update. In the `UpdateErr` state, the DNSConnector controller keeps the last known good DNS zone version, ensuring uninterrupted name resolution. 129 | * `Pending` - The DNSZone has been created and passed the syntax validation check. It is waiting to be picked up by the DNSConnector controller. 130 | 131 | ### Status Example 132 | ```json 133 | { 134 | "conditions": [ 135 | { 136 | "lastTransitionTime": "2024-06-04T00:40:11Z", 137 | "message": "Picked up by DNSConnector", 138 | "observedGeneration": 7, 139 | "reason": "Active", 140 | "status": "True", 141 | "type": "Ready" 142 | } 143 | ], 144 | "currentZoneSerial": "0603194011", 145 | "recordCount": 0, 146 | "validationPassed": true, 147 | "zoneConfigmap": "coredns-zone-market-example-zone" 148 | } 149 | ``` 150 | 151 | ## Troubleshoot 152 | 153 | ### DNSZone in UpdateError state. 154 | 155 | Typically occurs when the DNSZone didn't pass the syntax validation test. It most probably That could happen because of the bad patch made to the DNSZone specs. It's worth noting that even in a degraded state, CoreDNS retains the last "good" zone configuration. This ensures that all previously configured records remain accessible, mitigating the impact on DNS resolution. 156 | 157 | ```sh 158 | $ kubectl describe dnszones market-example-zone 159 | Name: market-example-zone 160 | Namespace: kube-system 161 | Labels: 162 | Annotations: 163 | API Version: monkale.monkale.io/v1alpha1 164 | Kind: DNSZone 165 | Metadata: 166 | Creation Timestamp: 2024-06-02T19:50:47Z 167 | Finalizers: 168 | dnszones/finalizers 169 | Generation: 5 170 | Resource Version: 35984 171 | UID: 74d11659-ff73-4197-9f09-499e99a22afd 172 | Spec: 173 | Cm Prefix: coredns-zone- 174 | Connector Name: coredns 175 | Domain: market.example.com 176 | Expire Time: 1209600 177 | Minimum TTL: 86400 178 | Primary NS: 179 | Hostname: ns1 180 | Ip Address: 10.120.120.10.254 181 | Record Type: A 182 | Refrash Rate: 7200 183 | Resp Person Email: admin@example.com 184 | Retry Interval: 3600 185 | Ttl: 86400 186 | Status: 187 | Conditions: 188 | Last Transition Time: 2024-06-03T14:38:58Z 189 | Message: Zone validation failure. Preserving the previous version. Error: error parsing records: dns: bad A A: "10.120.120.10.254" at line: 11:26 190 | Observed Generation: 5 191 | Reason: UpdateError 192 | Status: False 193 | Type: Ready 194 | Current Zone Serial: 0603092425 195 | Record Count: 15 196 | Validation Passed: true 197 | Zone Configmap: coredns-zone-market-example-zone 198 | Events: 199 | ``` 200 | 201 | ### DnsZone is Pending state 202 | A DNSZone enters a pending state when the zonefile has been created and is awaiting pickup by the DNSConnector. This typically occurs during the synchronization process between the DNSZone and the DNSConnector. 203 | 204 | Additionally, a DNSZone might remain pending if it's deployed in an incorrect namespace. It's crucial to ensure that all operator-related resources, including DNSZone, are installed in the same namespace as your CoreDNS server. 205 | 206 | Describe the DNSZone resource. Verify that the `spec.connectorName` points to the correct DNSConnector responsible for syncing the zonefile. If the connectorName is correct, proceed to describe the DNSConnector and review the controller logs for further insights. 207 | 208 | ```sh 209 | $ kubectl describe dnszones no-connector-test-zone 210 | Name: no-connector-test-zone 211 | Namespace: kube-system 212 | Labels: 213 | Annotations: 214 | API Version: monkale.monkale.io/v1alpha1 215 | Kind: DNSZone 216 | Metadata: 217 | Creation Timestamp: 2024-06-02T18:44:59Z 218 | Finalizers: 219 | dnszones/finalizers 220 | Generation: 1 221 | Resource Version: 622 222 | UID: 84efd361-f8d8-41a1-962b-0de16942fe05 223 | Spec: 224 | Cm Prefix: coredns-zone- 225 | Connector Name: this-connector-does-not-exist 226 | Domain: test.com 227 | Expire Time: 1209600 228 | Minimum TTL: 86400 229 | Primary NS: 230 | Hostname: ns1 231 | Ip Address: 10.120.100.11 232 | Record Type: A 233 | Refrash Rate: 7200 234 | Resp Person Email: admin@test.local 235 | Retry Interval: 3600 236 | Ttl: 86400 237 | Status: 238 | Conditions: 239 | Last Transition Time: 2024-06-02T18:45:00Z 240 | Message: Zone ConfigMap has been created: coredns-zone-no-connector-test-zone 241 | Observed Generation: 1 242 | Reason: Pending 243 | Status: True 244 | Type: Ready 245 | Current Zone Serial: 0602134500 246 | Record Count: 0 247 | Validation Passed: true 248 | Zone Configmap: coredns-zone-no-connector-test-zone 249 | Events: 250 | ``` -------------------------------------------------------------------------------- /docs/qa/pics/describe-coredns-mounts-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monkale-io/coredns-manager-operator/29157f89f232d0106a2551ef2a901b9c8009f7b2/docs/qa/pics/describe-coredns-mounts-1.png -------------------------------------------------------------------------------- /docs/qa/pics/describe-corefile-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monkale-io/coredns-manager-operator/29157f89f232d0106a2551ef2a901b9c8009f7b2/docs/qa/pics/describe-corefile-1.png -------------------------------------------------------------------------------- /docs/troubleshoot.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | When you're troubleshooting the coredns-manager-operator, your go-to tools are `kubectl get` and `kubectl describe`. These commands help you check the status of the resources involved. 3 | 4 | The coredns-manager-operator is made up of different custom resources in your Kubernetes cluster, all linked together. Any issues affecting these resources will show up in their status when you run `kubectl get`. For more details on a specific resource, use `kubectl describe`. If these steps don't solve your problem, then you can check the logs. 5 | 6 | ## Wrong Namespace 7 | 8 | **>IMPORTANT:** Make sure to deploy the coredns-manager-operator and its resources in the same namespace as CoreDNS, usually `kube-system`. Always include the `--namespace kube-system` 9 | 10 | 11 | 12 | # DNSRecords Troubleshoot 13 | 14 | * [DNSZones Guide - Troubleshoot](dnszones.md#Troubleshoot) 15 | 16 | * [Troubleshoot Guide - Nameresolution Troubleshoot](troubleshoot.md#nameresolution-troubleshoot) 17 | 18 | **Hint:** Remember that Specs of DNSRecords are templated into the standardized RFC1035 Zonefile entry format. They are templated using the following template: `{{ .Spec.Record.Name }} {{ .Spec.Record.TTL }} IN {{ .Spec.Record.Type }} {{ .Spec.Record.Value -}}`. Then, they are checked for syntax errors. However, they do not check for logic errors. If a record seems `Ready` but cannot be resolved, investigate potential logic issues in your records. Refer to the "Nameresolution Troubleshoot" section for further guidance. 19 | 20 | 21 | # DNSZones Troubleshoot 22 | 23 | [DNSZones Guide - Troubleshoot](dnszones.md#Troubleshoot) 24 | 25 | **Hint:** DNSZone specifications are used to template the Start of Authority (SOA) record and primary NS (Name Server) record for the zone. 26 | 27 | 28 | 29 | # DNSConnectors Troubleshoot 30 | 31 | [DNSConnector Guide - Troubleshoot](dnsconnector.md#Troubleshoot) 32 | 33 | * DNSConnector serves as the bridge between coredns-manager-operator and Kubernetes' CoreDNS. It references the coredns deployment configMap and the coredns deployment itself. If `kubectl get dnsconnectors` indicates any misconfiguration, it requires prompt attention. 34 | 35 | * DNSConnector issues commonly arise during initial implementation of the coredns-manager-operator or due to incorrect patching procedures. Regular monitoring and thorough validation are essential to prevent and resolve such issues effectively. 36 | 37 | * It's crucial to recognize that certain types of misconfigurations could potentially cause the operator to crash. 38 | 39 | 40 | 41 | # Nameresolution Troubleshoot 42 | 43 | * Individual records and the entire zone file undergo syntax checks, but logic errors are not verified. If a record appears as **"Ready"** but remains unresolved, investigate potential logic issues within your records. 44 | 45 | * The operator generates `coredns-zone-` configMaps and attaches them to CoreDNS. For troubleshooting, utilize `kubectl describe cm coredns-zone-` and `kubectl describe dnsrecord `. 46 | 47 | * Remember, the coredns-manager-operator is not a DNS server but rather a tool for templating zone files using DNSZone and DNSRecord resources per RFC1035 standards. Handle debugging as you would with any other DNS server, then apply fixes to DNSRecord or DNSZone accordingly. 48 | 49 | ## Networking issues 50 | 51 | The project is designed to manage CoreDNS zones, but it does not expose CoreDNS to the network by default. Administrators must handle exposure themselves using methods such as LoadBalancer services, iptables, NodePorts, HAProxy, IngressRoutes or even kubectl port-forwarding. 52 | 53 | ## Resolve name resolution 54 | 55 | In this example, the record `www.market.example.com` appears correct at first glance, with an expected resolution to `10.100.100.10`. However, despite this apparent correctness, the record cannot be resolved for an unknown reason. 56 | 57 | ```sh 58 | # zone domain is market.example.com 59 | market-example-zone market.example.com 15 2024-06-03T17:23:03Z 0603172349 Active 60 | 61 | # record is Ready 62 | $ kubectl get dnsrecords.monkale.monkale.io www-a-market-example 63 | NAME RECORD NAME RECORD TYPE RECORD VALUE ZONE REFERENCE LAST CHANGE STATE 64 | www-a-market-example www.market.example.com A 10.100.100.10 market-example-zone 2024-06-03T16:32:54Z Ready 65 | 66 | # cannot resolve 67 | $ dig @192.168.122.10 www.market.example.com 68 | 69 | ; <<>> DiG 9.18.26 <<>> @192.168.122.10 www.market.example.com 70 | ; (1 server found) 71 | ;; global options: +cmd 72 | ;; Got answer: 73 | ;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 20668 74 | ;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1 75 | ;; WARNING: recursion requested but not available 76 | 77 | ;; OPT PSEUDOSECTION: 78 | ; EDNS: version: 0, flags:; udp: 1232 79 | ; COOKIE: a15c22a7e6a3cd00 (echoed) 80 | ;; QUESTION SECTION: 81 | ;www.market.example.com. IN A 82 | 83 | ;; AUTHORITY SECTION: 84 | market.example.com. 86400 IN SOA ns1.market.example.com. admin\@example.com. 603172349 7200 3600 1209600 86400 85 | 86 | ;; Query time: 1 msec 87 | ;; SERVER: 192.168.122.10#53(192.168.122.10) (UDP) 88 | ;; WHEN: Mon Jun 03 12:24:57 CDT 2024 89 | ;; MSG SIZE rcvd: 156 90 | ``` 91 | 92 | The problem is that the user has configured the record `www.market.example.com` without indicating it as a fully qualified domain name (FQDN) by adding a `.` at the end. Consequently, the DNS server interprets it as `www.market.example.com.market.example.com` due to the default ORIGIN of `market.example.com`. 93 | 94 | To resolve this issue, there are two solutions: 95 | 96 | 1. Set Record as FQDN: 97 | 98 | Modify the record to www.market.example.com. by adding a `.` at the end to indicate it as a fully qualified domain name. 99 | 100 | 2. Remove Domain Name from Record: 101 | 102 | Alternatively, remove the domain name from the record, leaving only www. 103 | 104 | Here's an example of Solution 1: 105 | ```sh 106 | # patch record to fqdn 107 | $ kubectl patch DNSRecord www-a-market-example --type='merge' -p='{"spec":{"record": {"name": "www.market.example.com."}}}' 108 | dnsrecord.monkale.monkale.io/www-a-market-example patched 109 | 110 | # record name is not fqdn 111 | $ kubectl get DNSRecord www-a-market-example 112 | NAME RECORD NAME RECORD TYPE RECORD VALUE ZONE REFERENCE LAST CHANGE STATE 113 | www-a-market-example www.market.example.com. A 10.100.100.10 market-example-zone 2024-06-03T17:26:58Z Ready 114 | 115 | # works 116 | $ dig +short @192.168.122.10 www.market.example.com 117 | 10.100.100.10 118 | ``` 119 | 120 | Here's an example of Solution 2: 121 | ```sh 122 | # remove domain and trailing dot 123 | $ kubectl patch DNSRecord www-a-market-example --type='merge' -p='{"spec":{"record": {"name": "www"}}}' 124 | dnsrecord.monkale.monkale.io/www-a-market-example patched 125 | 126 | # get resource 127 | $ kubectl get DNSRecord www-a-market-example 128 | NAME RECORD NAME RECORD TYPE RECORD VALUE ZONE REFERENCE LAST CHANGE STATE 129 | www-a-market-example www A 10.100.100.10 market-example-zone 2024-06-03T17:29:25Z Ready 130 | 131 | # works 132 | $ dig +short @192.168.122.10 www.market.example.com 133 | 10.100.100.10 134 | ``` 135 | 136 | 137 | # Operator Level Issues 138 | 139 | To troubleshoot operator-level issues, describe the DNSConnector resource and review its logs. Report any findings for further investigation and resolution. This approach helps to identify and address issues promptly, ensuring the smooth operation of the DNS management system. 140 | 141 | However, they do not check for logic errors. -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/monkale.io/coredns-manager-operator 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/miekg/dns v1.1.59 7 | github.com/onsi/ginkgo/v2 v2.9.5 8 | github.com/onsi/gomega v1.27.7 9 | k8s.io/api v0.27.2 10 | k8s.io/apimachinery v0.27.2 11 | k8s.io/client-go v0.27.2 12 | sigs.k8s.io/controller-runtime v0.15.0 13 | ) 14 | 15 | require ( 16 | github.com/beorn7/perks v1.0.1 // indirect 17 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/emicklei/go-restful/v3 v3.9.0 // indirect 20 | github.com/evanphx/json-patch/v5 v5.6.0 // indirect 21 | github.com/fsnotify/fsnotify v1.6.0 // indirect 22 | github.com/go-logr/logr v1.2.4 // indirect 23 | github.com/go-logr/zapr v1.2.4 // indirect 24 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 25 | github.com/go-openapi/jsonreference v0.20.1 // indirect 26 | github.com/go-openapi/swag v0.22.3 // indirect 27 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect 28 | github.com/gogo/protobuf v1.3.2 // indirect 29 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 30 | github.com/golang/protobuf v1.5.3 // indirect 31 | github.com/google/gnostic v0.5.7-v3refs // indirect 32 | github.com/google/go-cmp v0.5.9 // indirect 33 | github.com/google/gofuzz v1.1.0 // indirect 34 | github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect 35 | github.com/google/uuid v1.3.0 // indirect 36 | github.com/imdario/mergo v0.3.6 // indirect 37 | github.com/josharian/intern v1.0.0 // indirect 38 | github.com/json-iterator/go v1.1.12 // indirect 39 | github.com/mailru/easyjson v0.7.7 // indirect 40 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 41 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 42 | github.com/modern-go/reflect2 v1.0.2 // indirect 43 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 44 | github.com/pkg/errors v0.9.1 // indirect 45 | github.com/prometheus/client_golang v1.15.1 // indirect 46 | github.com/prometheus/client_model v0.4.0 // indirect 47 | github.com/prometheus/common v0.42.0 // indirect 48 | github.com/prometheus/procfs v0.9.0 // indirect 49 | github.com/spf13/pflag v1.0.5 // indirect 50 | go.uber.org/atomic v1.7.0 // indirect 51 | go.uber.org/multierr v1.6.0 // indirect 52 | go.uber.org/zap v1.24.0 // indirect 53 | golang.org/x/mod v0.16.0 // indirect 54 | golang.org/x/net v0.22.0 // indirect 55 | golang.org/x/oauth2 v0.5.0 // indirect 56 | golang.org/x/sys v0.18.0 // indirect 57 | golang.org/x/term v0.18.0 // indirect 58 | golang.org/x/text v0.14.0 // indirect 59 | golang.org/x/time v0.3.0 // indirect 60 | golang.org/x/tools v0.19.0 // indirect 61 | gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect 62 | google.golang.org/appengine v1.6.7 // indirect 63 | google.golang.org/protobuf v1.30.0 // indirect 64 | gopkg.in/inf.v0 v0.9.1 // indirect 65 | gopkg.in/yaml.v2 v2.4.0 // indirect 66 | gopkg.in/yaml.v3 v3.0.1 // indirect 67 | k8s.io/apiextensions-apiserver v0.27.2 // indirect 68 | k8s.io/component-base v0.27.2 // indirect 69 | k8s.io/klog/v2 v2.90.1 // indirect 70 | k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect 71 | k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect 72 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 73 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 74 | sigs.k8s.io/yaml v1.3.0 // indirect 75 | ) 76 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 monkale.io. 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 | http://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 | */ -------------------------------------------------------------------------------- /internal/controller/dnsconnector_construction.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 monkale.io. 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 | http://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 controller 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | 23 | appsv1 "k8s.io/api/apps/v1" 24 | corev1 "k8s.io/api/core/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "sigs.k8s.io/controller-runtime/pkg/client" 27 | 28 | monkalev1alpha1 "github.com/monkale.io/coredns-manager-operator/api/v1alpha1" 29 | ) 30 | 31 | // generateCorefile is used to generate Corefile based on originalCorefile(string) and DNSZone's zonefile configMaps. 32 | // receives original corednsConfCM and zoneConfigMaps as args. 33 | func generateCorefileCM(dnsConnector *monkalev1alpha1.DNSConnector, corednsConfCM *corev1.ConfigMap, zoneConfigMaps *corev1.ConfigMapList) (corev1.ConfigMap, error) { 34 | corefileConfigBlockStartPrefix := "# COREDNS CONTROLLER MANAGED BLOCK BEGINNING -- " 35 | corefileConfigBlockEndPrefix := "# COREDNS CONTROLLER MANAGED BLOCK END -- " 36 | corefileBlocks := make(map[string]string) 37 | configMapDomains := make(map[string]bool) 38 | var newCorefileBuilder strings.Builder 39 | 40 | cmDataKey := dnsConnector.Spec.CorednsCM.CorefileKey 41 | newCorednsConfCM := corednsConfCM.DeepCopy() 42 | 43 | corednsCorefileContent, ok := corednsConfCM.Data[cmDataKey] 44 | if !ok { 45 | return corev1.ConfigMap{}, fmt.Errorf("key %s not found in CoreDNS ConfigMap", cmDataKey) 46 | } 47 | 48 | for _, configMap := range zoneConfigMaps.Items { 49 | // extract domain 50 | domainName, ok := configMap.Annotations["DomainName"] 51 | if !ok { 52 | return corev1.ConfigMap{}, fmt.Errorf("configMap %s does not have a domain annotation", configMap.Name) 53 | } 54 | 55 | // get enabled plugins 56 | pluginString := "" 57 | for _, plugin := range dnsConnector.Spec.CorednsZoneEnaledPlugins { 58 | pluginString += fmt.Sprintf("\n\t%s", plugin) 59 | } 60 | 61 | // Ensure that zonefile contains zonefile in the cm.data 62 | zonefileName := monkalev1alpha1.EnsureFQDN(domainName) + "zone" 63 | if _, ok := configMap.Data[zonefileName]; !ok { 64 | return corev1.ConfigMap{}, fmt.Errorf("configMap %s does not contain zonefile data", configMap.Name) 65 | } 66 | 67 | // generate zone config block 68 | configBlock := fmt.Sprintf(` 69 | %s %s 70 | %s:53 { 71 | file %s/%s%s 72 | } 73 | %s %s`, corefileConfigBlockStartPrefix, domainName, domainName, dnsConnector.Spec.CorednsDeployment.ZoneFileMountDir, zonefileName, pluginString, corefileConfigBlockEndPrefix, domainName) 74 | 75 | corefileBlocks[domainName] = configBlock 76 | configMapDomains[domainName] = true 77 | } 78 | 79 | // iterate over corefile 80 | lines := strings.Split(corednsCorefileContent, "\n") 81 | inBlock := false 82 | 83 | for _, line := range lines { 84 | trimmedLine := strings.TrimSpace(line) 85 | if strings.HasPrefix(trimmedLine, corefileConfigBlockStartPrefix) { 86 | inBlock = true 87 | currentDomain := strings.TrimPrefix(trimmedLine, corefileConfigBlockStartPrefix) 88 | if _, exists := corefileBlocks[currentDomain]; exists { 89 | newCorefileBuilder.WriteString(corefileBlocks[currentDomain]) 90 | delete(corefileBlocks, currentDomain) 91 | } 92 | } else if strings.HasPrefix(trimmedLine, corefileConfigBlockEndPrefix) { 93 | inBlock = false 94 | } else if !inBlock { 95 | newCorefileBuilder.WriteString(line + "\n") 96 | } 97 | } 98 | 99 | for _, block := range corefileBlocks { 100 | newCorefileBuilder.WriteString(block) 101 | } 102 | 103 | corefileContent := newCorefileBuilder.String() 104 | // modify configmap 105 | newCorednsConfCM.Data[cmDataKey] = corefileContent 106 | 107 | return *newCorednsConfCM, nil 108 | } 109 | 110 | // getDesiredVolumes iterates over zone configmaps list and returns a map where the key is volume name based on the 111 | // domain name annotation, and values are two string: configMap.Name and configmap.Data zone key 112 | func getDesiredVolumes(configMaps *corev1.ConfigMapList) (map[string][2]string, error) { 113 | desiredVolumes := make(map[string][2]string) 114 | for _, configMap := range configMaps.Items { 115 | domainName, ok := configMap.Annotations["DomainName"] 116 | if !ok { 117 | return nil, fmt.Errorf("configMap %s does not have a domain annotation", configMap.Name) 118 | } 119 | volumeName := fmt.Sprintf("dnszone-%s", strings.ReplaceAll(domainName, ".", "-")) 120 | volumeName = strings.TrimSuffix(volumeName, "-") 121 | 122 | if len(configMap.Data) != 1 { 123 | return nil, fmt.Errorf("configMap %s should contain only one key", configMap.Name) 124 | } 125 | var cmZoneKey string 126 | for k := range configMap.Data { 127 | cmZoneKey = k 128 | break 129 | } 130 | 131 | desiredVolumes[volumeName] = [2]string{configMap.Name, cmZoneKey} 132 | } 133 | return desiredVolumes, nil 134 | } 135 | 136 | // setZoneFileConifgMaps attaches ConfigMaps to a CoreDNS deployment by adding them as volumes 137 | // and volume mounts to the PodTemplateSpec of the provided StatefulSet, Deployment, or DaemonSet. 138 | // It returns the modified deployment object. 139 | func setZoneFileConifgMaps(dnsConnector monkalev1alpha1.DNSConnector, corednsDeployment client.Object, configMaps *corev1.ConfigMapList) (client.Object, error) { 140 | var podTemplateSpec *corev1.PodTemplateSpec 141 | switch res := corednsDeployment.(type) { 142 | case *appsv1.StatefulSet: 143 | podTemplateSpec = &res.Spec.Template 144 | case *appsv1.Deployment: 145 | podTemplateSpec = &res.Spec.Template 146 | case *appsv1.DaemonSet: 147 | podTemplateSpec = &res.Spec.Template 148 | default: 149 | return nil, fmt.Errorf("unsupported resource type: %T", res) 150 | } 151 | 152 | // trigger coredns deployment reconciliation 153 | if podTemplateSpec.Annotations == nil { 154 | podTemplateSpec.Annotations = make(map[string]string) 155 | } 156 | podTemplateSpec.Annotations["reconcilation-request"] = fmt.Sprintf("%d", metav1.Now().Unix()) 157 | 158 | // get desired volumes from the provided configmaps 159 | desiredVolumes, err := getDesiredVolumes(configMaps) 160 | if err != nil { 161 | return nil, err 162 | } 163 | 164 | // filter exisitng volumes and volume mounts to remove old DNS zone volume 165 | newVolumes := make([]corev1.Volume, 0) 166 | newVolumeMounts := make([]corev1.VolumeMount, 0) 167 | for _, volume := range podTemplateSpec.Spec.Volumes { 168 | if strings.HasPrefix(volume.Name, "dnszone-") { 169 | if _, exists := desiredVolumes[volume.Name]; exists { 170 | newVolumes = append(newVolumes, volume) // keep desired volume 171 | } 172 | } else { 173 | newVolumes = append(newVolumes, volume) // keep other volume 174 | } 175 | } 176 | 177 | for _, volumeMount := range podTemplateSpec.Spec.Containers[0].VolumeMounts { 178 | if strings.HasPrefix(volumeMount.Name, "dnszone-") { 179 | if _, exists := desiredVolumes[volumeMount.Name]; exists { 180 | newVolumeMounts = append(newVolumeMounts, volumeMount) // keep desired volumemount 181 | } 182 | } else { 183 | newVolumeMounts = append(newVolumeMounts, volumeMount) // keep other volume mounts 184 | } 185 | } 186 | 187 | // init volume and volumemounts 188 | for volumeName, value := range desiredVolumes { 189 | configMapName := value[0] 190 | cmZoneKey := value[1] 191 | volume := corev1.Volume{ 192 | Name: volumeName, 193 | VolumeSource: corev1.VolumeSource{ 194 | ConfigMap: &corev1.ConfigMapVolumeSource{ 195 | LocalObjectReference: corev1.LocalObjectReference{ 196 | Name: configMapName, 197 | }, 198 | Items: []corev1.KeyToPath{ 199 | { 200 | Key: cmZoneKey, 201 | Path: cmZoneKey, 202 | }, 203 | }, 204 | }, 205 | }, 206 | } 207 | volumeMount := corev1.VolumeMount{ 208 | Name: volumeName, 209 | MountPath: fmt.Sprintf("%s/%s", dnsConnector.Spec.CorednsDeployment.ZoneFileMountDir, cmZoneKey), 210 | SubPath: cmZoneKey, 211 | ReadOnly: true, 212 | } 213 | // Check if the volume already exists 214 | volumeExists := false 215 | for _, v := range newVolumes { 216 | if v.Name == volumeName { 217 | volumeExists = true 218 | break 219 | } 220 | } 221 | 222 | // Check if the volume mount already exists 223 | volumeMountExists := false 224 | for _, vm := range newVolumeMounts { 225 | if vm.MountPath == volumeMount.MountPath { 226 | volumeMountExists = true 227 | break 228 | } 229 | } 230 | // Add the volume and volume mount if they don't already exist 231 | if !volumeExists { 232 | newVolumes = append(newVolumes, volume) 233 | } 234 | if !volumeMountExists { 235 | newVolumeMounts = append(newVolumeMounts, volumeMount) 236 | } 237 | } 238 | 239 | // update podtemplate with new volumes and volumemount 240 | podTemplateSpec.Spec.Volumes = newVolumes 241 | for i := range podTemplateSpec.Spec.Containers { 242 | podTemplateSpec.Spec.Containers[i].VolumeMounts = newVolumeMounts 243 | } 244 | 245 | return corednsDeployment, nil 246 | } 247 | -------------------------------------------------------------------------------- /internal/controller/dnsrecord_construction.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "strings" 8 | "text/template" 9 | 10 | "github.com/miekg/dns" 11 | monkalev1alpha1 "github.com/monkale.io/coredns-manager-operator/api/v1alpha1" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | ) 14 | 15 | const recordHasBeenConstructedMsg string = "Record has been constructed. Awaiting for the dnszone controller to pick up the record" 16 | 17 | // handleGenericRecord - handling of all dns records are basically the same. 18 | func (r *DNSRecordReconciler) handleGenericRecord(ctx context.Context, dnsRecord *monkalev1alpha1.DNSRecord) (string, error) { 19 | previousState := dnsRecord.DeepCopy() 20 | // construct 21 | record, err := constructRecord(*dnsRecord) 22 | if err != nil { 23 | return "", fmt.Errorf("handle Record Error: %v", err) 24 | } 25 | 26 | // validate record and refresh&update status 27 | if err := validateRecords(record); err != nil { 28 | if err := r.refreshDNSRecordResource(ctx, previousState); err != nil { 29 | return "", fmt.Errorf("failed to refresh DNSRecord resource: %v", err) 30 | } 31 | message := fmt.Sprintf("Record validation failure: %s", err) 32 | setDnsRecordCondition(dnsRecord, metav1.ConditionFalse, monkalev1alpha1.ConditionReasonRecordDegraded, message) 33 | dnsRecord.Status.GeneratedRecord = record 34 | dnsRecord.Status.ValidationPassed = false 35 | if err := r.dnsRecordUpdateStatus(ctx, previousState, dnsRecord); err != nil { 36 | return "", fmt.Errorf("failed to update status and condition: %v", err) 37 | } 38 | return "", fmt.Errorf("record validation failure: %v", err) 39 | } 40 | 41 | // refresh&update status 42 | if err := r.refreshDNSRecordResource(ctx, previousState); err != nil { 43 | return "", fmt.Errorf("failed to refresh DNSRecord resource: %v", err) 44 | } 45 | setDnsRecordCondition(dnsRecord, metav1.ConditionFalse, monkalev1alpha1.ConditionReasonRecordPending, recordHasBeenConstructedMsg) 46 | dnsRecord.Status.GeneratedRecord = record 47 | dnsRecord.Status.ValidationPassed = true 48 | if err := r.dnsRecordUpdateStatus(ctx, previousState, dnsRecord); err != nil { 49 | return "", fmt.Errorf("failed to update status and condition: %v", err) 50 | } 51 | return record, nil 52 | } 53 | 54 | // constructRecord templates DNS record according to RFC1035. 55 | func constructRecord(dnsRecord monkalev1alpha1.DNSRecord) (string, error) { 56 | dnsRec := dnsRecord.DeepCopy() 57 | 58 | recordTmpl := `{{ .Spec.Record.Name }}{{ if .Spec.Record.TTL }} {{ .Spec.Record.TTL }}{{ end }} IN {{ .Spec.Record.Type }} {{ .Spec.Record.Value -}}` 59 | tmpl, err := template.New("record").Parse(recordTmpl) 60 | if err != nil { 61 | return "", fmt.Errorf("could not template DNS Record: %v", err) 62 | } 63 | 64 | var result bytes.Buffer 65 | if err := tmpl.Execute(&result, dnsRec); err != nil { 66 | return "", fmt.Errorf("could not template DNS Record: %v", err) 67 | } 68 | return result.String(), nil 69 | } 70 | 71 | // validateRecords performs syntax check of DNSRecords provided as a string. 72 | func validateRecords(records string) error { 73 | recordReader := strings.NewReader(records) 74 | recordParser := dns.NewZoneParser(recordReader, ".", "") 75 | for { 76 | _, ok := recordParser.Next() 77 | if !ok { 78 | break 79 | } 80 | if err := recordParser.Err(); err != nil { 81 | return fmt.Errorf("error parsing record: %v", err) 82 | } 83 | } 84 | // Check for any final errors 85 | if err := recordParser.Err(); err != nil { 86 | return fmt.Errorf("error parsing records: %v", err) 87 | } 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /internal/controller/dnsrecord_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 monkale.io. 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 | http://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 controller 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | "fmt" 23 | 24 | "k8s.io/apimachinery/pkg/api/equality" 25 | apierrors "k8s.io/apimachinery/pkg/api/errors" 26 | "k8s.io/apimachinery/pkg/api/meta" 27 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 | "k8s.io/apimachinery/pkg/runtime" 29 | "k8s.io/apimachinery/pkg/types" 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/predicate" 35 | 36 | monkalev1alpha1 "github.com/monkale.io/coredns-manager-operator/api/v1alpha1" 37 | ) 38 | 39 | // DNSRecordReconciler reconciles a DNSRecord object 40 | type DNSRecordReconciler struct { 41 | client.Client 42 | Scheme *runtime.Scheme 43 | } 44 | 45 | // +kubebuilder:rbac:groups=monkale.monkale.io,resources=dnsrecords,verbs=get;list;watch;create;update;patch;delete 46 | // +kubebuilder:rbac:groups=monkale.monkale.io,resources=dnsrecords/status,verbs=get;update;patch 47 | // +kubebuilder:rbac:groups=monkale.monkale.io,resources=dnsrecords/finalizers,verbs=update 48 | 49 | // For more details, check Reconcile and its Result here: 50 | // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.15.0/pkg/reconcile 51 | func (r *DNSRecordReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 52 | _ = log.FromContext(ctx) 53 | var dnsRecord monkalev1alpha1.DNSRecord 54 | 55 | // Fetch DNSRecord from kubernetes 56 | if err := r.Get(ctx, req.NamespacedName, &dnsRecord); err != nil { 57 | if apierrors.IsNotFound(err) { 58 | return ctrl.Result{}, nil 59 | } else if !apierrors.IsNotFound(err) { 60 | log.Log.Error(err, "DNSRecord instance. Failed to get DNSRecord", "DNSRecord.Name", dnsRecord.Name, "DNSZone.Name", dnsRecord.Spec.DNSZoneRef.Name) 61 | return ctrl.Result{}, err 62 | } 63 | } 64 | 65 | // DNSRecord has been just created. Add finalizer. 66 | if !controllerutil.ContainsFinalizer(&dnsRecord, monkalev1alpha1.DnsRecorsFinalizerName) { 67 | dnsRecObj := types.NamespacedName{Name: dnsRecord.Name, Namespace: dnsRecord.Namespace} 68 | clientK8sObj := dnsRecord.DeepCopy() 69 | if err := addFinalizer(ctx, r.Client, dnsRecObj, clientK8sObj, monkalev1alpha1.DnsRecorsFinalizerName); err != nil { 70 | log.Log.Error(err, "DNSRecord instance. Failed to add finalizer", "DNSRecord.Name", dnsRecord.Name, "DNSZone.Name", dnsRecord.Spec.DNSZoneRef.Name) 71 | return ctrl.Result{}, err 72 | } 73 | return ctrl.Result{Requeue: true}, nil 74 | } 75 | 76 | // DNSRecord has been deleted. Reconcile Delete 77 | if !dnsRecord.DeletionTimestamp.IsZero() { 78 | log.Log.Info("DNSRecord instance. Record is being deleted", "DNSRecord.Name", dnsRecord.Name, "DNSZone.Name", dnsRecord.Spec.DNSZoneRef.Name) 79 | return r.reconcileDelete(ctx, &dnsRecord) 80 | } 81 | 82 | // DNSRecord has been created or updated. Reconsile create or update 83 | log.Log.Info("DNSRecord instance. Reconciling", "DNSRecord.Name", dnsRecord.Name, "DNSZone.Name", dnsRecord.Spec.DNSZoneRef.Name) 84 | return r.reconcileDNSRecord(ctx, &dnsRecord) 85 | } 86 | 87 | // reconcileDelete reconciles if DNSRecord resource has been removed. 88 | func (r *DNSRecordReconciler) reconcileDelete(ctx context.Context, dnsRecord *monkalev1alpha1.DNSRecord) (ctrl.Result, error) { 89 | _ = log.FromContext(ctx) 90 | // Remove finazlizer from DNSRecord 91 | dnsRecObj := types.NamespacedName{Name: dnsRecord.Name, Namespace: dnsRecord.Namespace} 92 | clientK8sObj := dnsRecord.DeepCopy() 93 | log.Log.Info("DNSRecord instance. DNSRecord is being deleted. Removing finalizer from DNSRecord", "DNSRecord.Name", dnsRecord.Name, "DNSZone.Name", dnsRecord.Spec.DNSZoneRef.Name) 94 | if err := removeFinalizer(ctx, r.Client, dnsRecObj, clientK8sObj, monkalev1alpha1.DnsRecorsFinalizerName); err != nil { 95 | log.Log.Error(err, "DNSRecord instance. Failed to delete finalizer", "DNSRecord.Name", dnsRecord.Name, "DNSZone.Name", dnsRecord.Spec.DNSZoneRef.Name) 96 | return ctrl.Result{}, err 97 | } 98 | 99 | // Done 100 | log.Log.Info("DNSRecord instance. The DNSRecord has been deleted.", "DNSRecord.Name", dnsRecord.Name, "DNSZone.Name", dnsRecord.Spec.DNSZoneRef.Name) 101 | return ctrl.Result{}, nil 102 | } 103 | 104 | // reconcileDNSRecord - processes DNSRecord. It generates the entry for the zonefile, performs syntax check and updates Status. 105 | func (r *DNSRecordReconciler) reconcileDNSRecord(ctx context.Context, dnsRecord *monkalev1alpha1.DNSRecord) (ctrl.Result, error) { 106 | _ = log.FromContext(ctx) 107 | recordType := dnsRecord.Spec.Record.Type 108 | switch recordType { 109 | case "A": 110 | _, err := r.handleGenericRecord(ctx, dnsRecord) 111 | if err != nil { 112 | log.Log.Error(err, "DNSRecord instance. Proccess Record Failure", "DNSRecord.Name", dnsRecord.Name, "DNSZone.Name", dnsRecord.Spec.DNSZoneRef.Name) 113 | return ctrl.Result{}, err 114 | } 115 | case "AAAA": 116 | _, err := r.handleGenericRecord(ctx, dnsRecord) 117 | if err != nil { 118 | log.Log.Error(err, "DNSRecord instance. Proccess Record Failure", "DNSRecord.Name", dnsRecord.Name, "DNSZone.Name", dnsRecord.Spec.DNSZoneRef.Name) 119 | return ctrl.Result{}, err 120 | } 121 | case "CNAME": 122 | _, err := r.handleGenericRecord(ctx, dnsRecord) 123 | if err != nil { 124 | log.Log.Error(err, "DNSRecord instance. Proccess Record Failure", "DNSRecord.Name", dnsRecord.Name, "DNSZone.Name", dnsRecord.Spec.DNSZoneRef.Name) 125 | return ctrl.Result{}, err 126 | } 127 | case "MX": 128 | _, err := r.handleGenericRecord(ctx, dnsRecord) 129 | if err != nil { 130 | log.Log.Error(err, "DNSRecord instance. Proccess Record Failure", "DNSRecord.Name", dnsRecord.Name, "DNSZone.Name", dnsRecord.Spec.DNSZoneRef.Name) 131 | return ctrl.Result{}, err 132 | } 133 | case "TXT": 134 | _, err := r.handleGenericRecord(ctx, dnsRecord) 135 | if err != nil { 136 | log.Log.Error(err, "DNSRecord instance. Proccess Record Failure", "DNSRecord.Name", dnsRecord.Name, "DNSZone.Name", dnsRecord.Spec.DNSZoneRef.Name) 137 | return ctrl.Result{}, err 138 | } 139 | case "NS": 140 | _, err := r.handleGenericRecord(ctx, dnsRecord) 141 | if err != nil { 142 | log.Log.Error(err, "DNSRecord instance. Proccess Record Failure", "DNSRecord.Name", dnsRecord.Name, "DNSZone.Name", dnsRecord.Spec.DNSZoneRef.Name) 143 | return ctrl.Result{}, err 144 | } 145 | case "PTR": 146 | _, err := r.handleGenericRecord(ctx, dnsRecord) 147 | if err != nil { 148 | log.Log.Error(err, "DNSRecord instance. Proccess Record Failure", "DNSRecord.Name", dnsRecord.Name, "DNSZone.Name", dnsRecord.Spec.DNSZoneRef.Name) 149 | return ctrl.Result{}, err 150 | } 151 | case "SRV": 152 | _, err := r.handleGenericRecord(ctx, dnsRecord) 153 | if err != nil { 154 | log.Log.Error(err, "DNSRecord instance. Proccess Record Failure", "DNSRecord.Name", dnsRecord.Name, "DNSZone.Name", dnsRecord.Spec.DNSZoneRef.Name) 155 | return ctrl.Result{}, err 156 | } 157 | case "CAA": 158 | _, err := r.handleGenericRecord(ctx, dnsRecord) 159 | if err != nil { 160 | log.Log.Error(err, "DNSRecord instance. Proccess Record Failure", "DNSRecord.Name", dnsRecord.Name, "DNSZone.Name", dnsRecord.Spec.DNSZoneRef.Name) 161 | return ctrl.Result{}, err 162 | } 163 | case "DNSKEY": 164 | _, err := r.handleGenericRecord(ctx, dnsRecord) 165 | if err != nil { 166 | log.Log.Error(err, "DNSRecord instance. Proccess Record Failure", "DNSRecord.Name", dnsRecord.Name, "DNSZone.Name", dnsRecord.Spec.DNSZoneRef.Name) 167 | return ctrl.Result{}, err 168 | } 169 | case "DS": 170 | _, err := r.handleGenericRecord(ctx, dnsRecord) 171 | if err != nil { 172 | log.Log.Error(err, "DNSRecord instance. Proccess Record Failure", "DNSRecord.Name", dnsRecord.Name, "DNSZone.Name", dnsRecord.Spec.DNSZoneRef.Name) 173 | return ctrl.Result{}, err 174 | } 175 | case "NAPTR": 176 | _, err := r.handleGenericRecord(ctx, dnsRecord) 177 | if err != nil { 178 | log.Log.Error(err, "DNSRecord instance. Proccess Record Failure", "DNSRecord.Name", dnsRecord.Name, "DNSZone.Name", dnsRecord.Spec.DNSZoneRef.Name) 179 | return ctrl.Result{}, err 180 | } 181 | case "RRSIG": 182 | _, err := r.handleGenericRecord(ctx, dnsRecord) 183 | if err != nil { 184 | log.Log.Error(err, "DNSRecord instance. Proccess Record Failure", "DNSRecord.Name", dnsRecord.Name, "DNSZone.Name", dnsRecord.Spec.DNSZoneRef.Name) 185 | return ctrl.Result{}, err 186 | } 187 | case "DNAME": 188 | _, err := r.handleGenericRecord(ctx, dnsRecord) 189 | if err != nil { 190 | log.Log.Error(err, "DNSRecord instance. Proccess Record Failure", "DNSRecord.Name", dnsRecord.Name, "DNSZone.Name", dnsRecord.Spec.DNSZoneRef.Name) 191 | return ctrl.Result{}, err 192 | } 193 | case "HINFO": 194 | _, err := r.handleGenericRecord(ctx, dnsRecord) 195 | if err != nil { 196 | log.Log.Error(err, "DNSRecord instance. Proccess Record Failure", "DNSRecord.Name", dnsRecord.Name, "DNSZone.Name", dnsRecord.Spec.DNSZoneRef.Name) 197 | return ctrl.Result{}, err 198 | } 199 | default: 200 | err := errors.New("unknown redord type: " + recordType) 201 | log.Log.Error(err, "DNSRecord instance. Bad record Type", "DNSRecord.Name", dnsRecord.Name, "DNSZone.Name", dnsRecord.Spec.DNSZoneRef.Name) 202 | return ctrl.Result{}, err 203 | } 204 | return ctrl.Result{}, nil 205 | } 206 | 207 | // setDnsRecordCondition adds or updates a given condition in the DNSRecord status. 208 | func setDnsRecordCondition(dnsRecord *monkalev1alpha1.DNSRecord, status metav1.ConditionStatus, reason string, message string) { 209 | now := metav1.Now() 210 | cond := metav1.Condition{ 211 | Type: string(monkalev1alpha1.ConditionRecordTypeReady), 212 | Status: status, 213 | LastTransitionTime: now, 214 | Reason: reason, 215 | Message: message, 216 | ObservedGeneration: dnsRecord.Generation, 217 | } 218 | meta.SetStatusCondition(&dnsRecord.Status.Conditions, cond) 219 | } 220 | 221 | // refreshDNSRecordResource fetch from kubernetes a new version of DNSRecordResource 222 | func (r *DNSRecordReconciler) refreshDNSRecordResource(ctx context.Context, dnsRecord *monkalev1alpha1.DNSRecord) error { 223 | dnsRecObj := types.NamespacedName{Name: dnsRecord.Name, Namespace: dnsRecord.Namespace} 224 | clientK8sObj := dnsRecord.DeepCopy() 225 | if err := getObjFromK8s(ctx, r.Client, dnsRecObj, clientK8sObj); err != nil { 226 | return fmt.Errorf("failed to refresh DNSRecord resource: %v", err) 227 | } 228 | return nil 229 | } 230 | 231 | // dnsRecordUpdateStatus updates the status of the DNSRecord Update Status, only if status has been changed 232 | func (r *DNSRecordReconciler) dnsRecordUpdateStatus(ctx context.Context, previous, current *monkalev1alpha1.DNSRecord) error { 233 | if !equality.Semantic.DeepEqual(previous.Status, current.Status) { 234 | if err := r.Status().Update(ctx, current); err != nil { 235 | return fmt.Errorf("failed to update status and condition: %v", err) 236 | } 237 | } 238 | return nil 239 | } 240 | 241 | // SetupWithManager sets up the controller with the Manager. 242 | // Watches https://book.kubebuilder.io/reference/watching-resources/externally-managed 243 | func (r *DNSRecordReconciler) SetupWithManager(mgr ctrl.Manager) error { 244 | return ctrl.NewControllerManagedBy(mgr). 245 | For(&monkalev1alpha1.DNSRecord{}). 246 | WithEventFilter(predicate.GenerationChangedPredicate{}). 247 | Complete(r) 248 | } 249 | -------------------------------------------------------------------------------- /internal/controller/dnszone_construction.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "sort" 8 | "strings" 9 | "text/template" 10 | 11 | corev1 "k8s.io/api/core/v1" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/apimachinery/pkg/fields" 14 | "sigs.k8s.io/controller-runtime/pkg/client" 15 | 16 | monkalev1alpha1 "github.com/monkale.io/coredns-manager-operator/api/v1alpha1" 17 | ) 18 | 19 | // bakedRecords represents the records that are members of the Zonefile. 20 | type bakedRecords struct { 21 | count int 22 | recordsString string 23 | } 24 | 25 | // constructZoneFile - constructs and validates Zone. 26 | func constructZoneFile(dnsZone *monkalev1alpha1.DNSZone, records string, serialNumber string) (string, error) { 27 | var newZoneHeader string 28 | newZoneHeaderValues := monkalev1alpha1.DNSZoneHeader{ 29 | DomainName: monkalev1alpha1.EnsureFQDN(dnsZone.Spec.Domain), 30 | PrimaryNSHostname: dnsZone.Spec.PrimaryNS.Hostname, 31 | PrimaryNSIp: dnsZone.Spec.PrimaryNS.IPAddress, 32 | PrimaryNSType: dnsZone.Spec.PrimaryNS.RecordType, 33 | RespPerson: dnsZone.Spec.RespPersonEmail, 34 | ZoneTTL: dnsZone.Spec.TTL, 35 | Serial: serialNumber, 36 | Refresh: dnsZone.Spec.RefreshRate, 37 | Retry: dnsZone.Spec.RetryInterval, 38 | Expire: dnsZone.Spec.ExpireTime, 39 | MinimumTTL: dnsZone.Spec.MinimumTTL, 40 | } 41 | newZoneHeader, err := templateZoneHeader(newZoneHeaderValues) 42 | if err != nil { 43 | return "", fmt.Errorf("unable to template the zoneHeader: %v", err) 44 | } 45 | zonefileContent := newZoneHeader + records 46 | return zonefileContent, nil 47 | } 48 | 49 | // constructZoneConfigMap constructs config map for the Zone 50 | func constructZoneConfigMap(cmObj string, dnsZone *monkalev1alpha1.DNSZone, zonefileContent string, upcomingCMAnnotations map[string]string) (corev1.ConfigMap, error) { 51 | cm := corev1.ConfigMap{ 52 | ObjectMeta: metav1.ObjectMeta{ 53 | Name: cmObj, 54 | Namespace: dnsZone.ObjectMeta.Namespace, 55 | Labels: map[string]string{"app": "coredns-addon-operator"}, 56 | Annotations: upcomingCMAnnotations, 57 | OwnerReferences: []metav1.OwnerReference{ 58 | { 59 | APIVersion: dnsZone.APIVersion, 60 | Kind: dnsZone.Kind, 61 | Name: dnsZone.Name, 62 | UID: dnsZone.UID, 63 | }, 64 | }, 65 | }, 66 | Data: map[string]string{ 67 | monkalev1alpha1.EnsureFQDN(dnsZone.Spec.Domain) + "zone": zonefileContent, 68 | }, 69 | } 70 | return cm, nil 71 | } 72 | 73 | // templateZoneHeader builds Zone header: SOA and first NS A record 74 | func templateZoneHeader(header monkalev1alpha1.DNSZoneHeader) (string, error) { 75 | zoneTmpl := `$ORIGIN {{.DomainName}} 76 | $TTL {{ .ZoneTTL }}s 77 | @ IN SOA {{.PrimaryNSHostname}}.{{.DomainName}} {{.RespPerson}}. ( 78 | {{.Serial}} ; Serial 79 | {{.Refresh}} ; Refresh 80 | {{.Retry}} ; Retry 81 | {{.Expire}} ; Expire 82 | {{.MinimumTTL}} ; Minimum TTL 83 | ) 84 | @ IN NS {{.PrimaryNSHostname}}.{{.DomainName}} 85 | {{.PrimaryNSHostname}} IN {{.PrimaryNSType}} {{.PrimaryNSIp}} 86 | ` 87 | tmpl, err := template.New("HEADER").Parse(zoneTmpl) 88 | if err != nil { 89 | return "", fmt.Errorf("could not template SOA or NS: %v", err) 90 | } 91 | 92 | var result bytes.Buffer 93 | if err := tmpl.Execute(&result, header); err != nil { 94 | return "", fmt.Errorf("could not template SOA or NS: %v", err) 95 | } 96 | zoneHeader := result.String() 97 | return zoneHeader, nil 98 | } 99 | 100 | // getGoodDnsRecords fetches all DNSRecords. fails if bad records found 101 | func (r *DNSZoneReconciler) getGoodDnsRecords(ctx context.Context, dnsZone *monkalev1alpha1.DNSZone) (monkalev1alpha1.DNSRecordList, error) { 102 | records := &monkalev1alpha1.DNSRecordList{} 103 | // list all DNSRecord with the same DNSZone. 104 | fieldSelector := fields.OneTermEqualSelector(monkalev1alpha1.DnsRecordIndex, dnsZone.Name) 105 | listOps := &client.ListOptions{ 106 | FieldSelector: fieldSelector, 107 | Namespace: dnsZone.Namespace, 108 | } 109 | if err := r.List(ctx, records, listOps); err != nil { 110 | return monkalev1alpha1.DNSRecordList{}, fmt.Errorf("could not list DNSRecords: %v", err) 111 | } 112 | 113 | // get only good records 114 | goodRecords := monkalev1alpha1.DNSRecordList{} 115 | for _, record := range records.Items { 116 | if record.Status.ValidationPassed { 117 | goodRecords.Items = append(goodRecords.Items, record) 118 | } 119 | } 120 | 121 | // sort records a-z 122 | sort.Slice(goodRecords.Items, func(i, j int) bool { 123 | return goodRecords.Items[i].Name < goodRecords.Items[j].Name 124 | }) 125 | 126 | return goodRecords, nil 127 | } 128 | 129 | // bakeRecords bakes DNSRecords into the single Zone file compatible string. 130 | func bakeRecords(dnsRecords monkalev1alpha1.DNSRecordList) (bakedRecords, error) { 131 | records := dnsRecords.Items 132 | var sb strings.Builder 133 | for i, record := range records { 134 | sb.WriteString(record.Status.GeneratedRecord) 135 | if i < len(records)-1 { 136 | sb.WriteString("\n") 137 | } 138 | } 139 | linesCount := len(strings.Split(sb.String(), "\n")) 140 | corednsEntries := bakedRecords{ 141 | count: linesCount, 142 | recordsString: sb.String(), 143 | } 144 | 145 | return corednsEntries, nil 146 | } 147 | -------------------------------------------------------------------------------- /internal/controller/finalizers.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 monkale.io. 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 | http://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 controller 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | "k8s.io/apimachinery/pkg/types" 24 | "sigs.k8s.io/controller-runtime/pkg/client" 25 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 26 | ) 27 | 28 | // Look up for k8s object, and add finalizer. 29 | func addFinalizer(ctx context.Context, cl client.Client, obj types.NamespacedName, resource client.Object, finalizerName string) error { 30 | // update resource by returned object from GetObjFromK8s 31 | if err := getObjFromK8s(ctx, cl, obj, resource); err != nil { 32 | return fmt.Errorf("failed to get resource %s/%s: %v", obj.Name, obj.Namespace, err) 33 | } 34 | if !controllerutil.ContainsFinalizer(resource, finalizerName) { 35 | // Add finalizer to object 36 | controllerutil.AddFinalizer(resource, finalizerName) 37 | if err := cl.Update(ctx, resource); err != nil { 38 | return fmt.Errorf("failed to get resource %s/%s with finalizer '%s': %v", obj.Name, obj.Namespace, finalizerName, err) 39 | } 40 | } 41 | return nil 42 | } 43 | 44 | // Look up for k8s object, and add finalizer. 45 | func removeFinalizer(ctx context.Context, cl client.Client, obj types.NamespacedName, resource client.Object, finalizerName string) error { 46 | // update resource by returned object from GetObjFromK8s 47 | if err := getObjFromK8s(ctx, cl, obj, resource); err != nil { 48 | return fmt.Errorf("failed to get resource %s/%s: %v", obj.Name, obj.Namespace, err) 49 | } 50 | 51 | if controllerutil.ContainsFinalizer(resource, finalizerName) { 52 | // Add finalizer to object 53 | controllerutil.RemoveFinalizer(resource, finalizerName) 54 | if err := cl.Update(ctx, resource); err != nil { 55 | return fmt.Errorf("failed to get resource %s/%s with finalizer '%s': %v", obj.Name, obj.Namespace, finalizerName, err) 56 | } 57 | } 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /internal/controller/obj_lookup.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 monkale.io. 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 | http://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 controller 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | appsv1 "k8s.io/api/apps/v1" 24 | "k8s.io/apimachinery/pkg/types" 25 | "sigs.k8s.io/controller-runtime/pkg/client" 26 | ) 27 | 28 | // Look up for object by resource name + name + namespace. Updates context. 29 | func getObjFromK8s(ctx context.Context, cl client.Client, obj types.NamespacedName, resource client.Object) error { 30 | if err := cl.Get(ctx, obj, resource); err != nil { 31 | return fmt.Errorf("failed to get resource %s/%s: %v", obj.Name, obj.Namespace, err) 32 | } 33 | return nil 34 | } 35 | 36 | // isStatefulSetReady checks if the StatefulSet is ready 37 | func isStatefulSetReady(sts *appsv1.StatefulSet) bool { 38 | return sts.Status.ReadyReplicas == *sts.Spec.Replicas 39 | } 40 | 41 | // isDeploymentReady checks if the Deployment is ready 42 | func isDeploymentReady(deploy *appsv1.Deployment) bool { 43 | return deploy.Status.ReadyReplicas == *deploy.Spec.Replicas 44 | } 45 | 46 | // isDaemonSetReady checks if the DaemonSet is ready 47 | func isDaemonSetReady(ds *appsv1.DaemonSet) bool { 48 | return ds.Status.NumberReady == ds.Status.DesiredNumberScheduled 49 | } 50 | -------------------------------------------------------------------------------- /internal/controller/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 monkale.io. 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 | http://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 controller 18 | 19 | import ( 20 | "path/filepath" 21 | "testing" 22 | 23 | . "github.com/onsi/ginkgo/v2" 24 | . "github.com/onsi/gomega" 25 | 26 | "k8s.io/client-go/kubernetes/scheme" 27 | "k8s.io/client-go/rest" 28 | "sigs.k8s.io/controller-runtime/pkg/client" 29 | "sigs.k8s.io/controller-runtime/pkg/envtest" 30 | logf "sigs.k8s.io/controller-runtime/pkg/log" 31 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 32 | 33 | monkalev1alpha1 "github.com/monkale.io/coredns-manager-operator/api/v1alpha1" 34 | //+kubebuilder:scaffold:imports 35 | ) 36 | 37 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 38 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 39 | 40 | var cfg *rest.Config 41 | var k8sClient client.Client 42 | var testEnv *envtest.Environment 43 | 44 | func TestControllers(t *testing.T) { 45 | RegisterFailHandler(Fail) 46 | 47 | RunSpecs(t, "Controller Suite") 48 | } 49 | 50 | var _ = BeforeSuite(func() { 51 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 52 | 53 | By("bootstrapping test environment") 54 | testEnv = &envtest.Environment{ 55 | CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, 56 | ErrorIfCRDPathMissing: true, 57 | } 58 | 59 | var err error 60 | // cfg is defined in this file globally. 61 | cfg, err = testEnv.Start() 62 | Expect(err).NotTo(HaveOccurred()) 63 | Expect(cfg).NotTo(BeNil()) 64 | 65 | err = monkalev1alpha1.AddToScheme(scheme.Scheme) 66 | Expect(err).NotTo(HaveOccurred()) 67 | 68 | //+kubebuilder:scaffold:scheme 69 | 70 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 71 | Expect(err).NotTo(HaveOccurred()) 72 | Expect(k8sClient).NotTo(BeNil()) 73 | 74 | }) 75 | 76 | var _ = AfterSuite(func() { 77 | By("tearing down the test environment") 78 | err := testEnv.Stop() 79 | Expect(err).NotTo(HaveOccurred()) 80 | }) 81 | --------------------------------------------------------------------------------