├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── api └── v1alpha1 │ ├── groupversion_info.go │ ├── namespace_types.go │ ├── preauthkey_types.go │ ├── server_types.go │ └── zz_generated.deepcopy.go ├── config ├── crd │ ├── bases │ │ ├── headscale.barpilot.io_namespaces.yaml │ │ ├── headscale.barpilot.io_preauthkeys.yaml │ │ └── headscale.barpilot.io_servers.yaml │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── patches │ │ ├── cainjection_in_namespaces.yaml │ │ ├── cainjection_in_preauthkeys.yaml │ │ ├── cainjection_in_servers.yaml │ │ ├── webhook_in_namespaces.yaml │ │ ├── webhook_in_preauthkeys.yaml │ │ └── webhook_in_servers.yaml ├── default │ ├── kustomization.yaml │ ├── manager_auth_proxy_patch.yaml │ └── manager_config_patch.yaml ├── manager │ ├── controller_manager_config.yaml │ ├── kustomization.yaml │ └── manager.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 │ ├── kustomization.yaml │ ├── leader_election_role.yaml │ ├── leader_election_role_binding.yaml │ ├── namespace_editor_role.yaml │ ├── namespace_viewer_role.yaml │ ├── preauthkey_editor_role.yaml │ ├── preauthkey_viewer_role.yaml │ ├── role.yaml │ ├── role_binding.yaml │ ├── server_editor_role.yaml │ ├── server_viewer_role.yaml │ └── service_account.yaml └── samples │ ├── assests │ └── issuer.yaml │ ├── headscale_v1alpha1_namespace.yaml │ ├── headscale_v1alpha1_preauthkey.yaml │ └── headscale_v1alpha1_server.yaml ├── controllers ├── default.go ├── namespace_controller.go ├── preauthkey_controller.go ├── server_controller.go ├── server_controller_test.go ├── suite_test.go └── testing-assets │ └── crd │ └── cert-manager.1.8.0.crds.yaml ├── go.mod ├── go.sum ├── hack └── boilerplate.go.txt ├── main.go └── pkg ├── headscale ├── config.go └── zz_generated.deepcopy.go └── utils ├── grpc.go └── utils.go /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | 4 | # Output of the go coverage tool, specifically when used with LiteIDE 5 | *.out 6 | 7 | bin/ 8 | testbin/ 9 | 10 | Dockerfile 11 | Makefile 12 | *.md 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin 9 | testbin/* 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 | *.swp 24 | *.swo 25 | *~ 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.18 as builder 3 | 4 | WORKDIR /workspace 5 | # Copy the Go Modules manifests 6 | COPY go.mod go.mod 7 | COPY go.sum go.sum 8 | # cache deps before building and copying source so that we don't need to re-download as much 9 | # and so that source changes don't invalidate our downloaded layer 10 | RUN go mod download 11 | 12 | # Copy the go source 13 | COPY . . 14 | 15 | # Build 16 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -ldflags="-s -w" -o manager main.go 17 | 18 | # Use distroless as minimal base image to package the manager binary 19 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 20 | FROM gcr.io/distroless/static:nonroot 21 | WORKDIR / 22 | COPY --from=builder /workspace/manager . 23 | USER 65532:65532 24 | 25 | ENTRYPOINT ["/manager"] 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | # Image URL to use all building/pushing image targets 3 | IMG ?= controller:latest 4 | # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. 5 | ENVTEST_K8S_VERSION = 1.23 6 | 7 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 8 | ifeq (,$(shell go env GOBIN)) 9 | GOBIN=$(shell go env GOPATH)/bin 10 | else 11 | GOBIN=$(shell go env GOBIN) 12 | endif 13 | 14 | # Setting SHELL to bash allows bash commands to be executed by recipes. 15 | # This is a requirement for 'setup-envtest.sh' in the test target. 16 | # Options are set to exit when a recipe line exits non-zero or a piped command fails. 17 | SHELL = /usr/bin/env bash -o pipefail 18 | .SHELLFLAGS = -ec 19 | 20 | .PHONY: all 21 | all: build 22 | 23 | ##@ General 24 | 25 | # The help target prints out all targets with their descriptions organized 26 | # beneath their categories. The categories are represented by '##@' and the 27 | # target descriptions by '##'. The awk commands is responsible for reading the 28 | # entire set of makefiles included in this invocation, looking for lines of the 29 | # file as xyz: ## something, and then pretty-format the target and help. Then, 30 | # if there's a line with ##@ something, that gets pretty-printed as a category. 31 | # More info on the usage of ANSI control characters for terminal formatting: 32 | # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 33 | # More info on the awk command: 34 | # http://linuxcommand.org/lc3_adv_awk.php 35 | 36 | .PHONY: help 37 | help: ## Display this help. 38 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 39 | 40 | ##@ Development 41 | 42 | .PHONY: manifests 43 | manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. 44 | $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases 45 | 46 | .PHONY: generate 47 | generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. 48 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 49 | 50 | .PHONY: fmt 51 | fmt: ## Run go fmt against code. 52 | go fmt ./... 53 | 54 | .PHONY: vet 55 | vet: ## Run go vet against code. 56 | go vet ./... 57 | 58 | .PHONY: test 59 | test: manifests generate fmt vet envtest ## Run tests. 60 | KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out 61 | 62 | ##@ Build 63 | 64 | .PHONY: build 65 | build: generate fmt vet ## Build manager binary. 66 | go build -ldflags="-s -w" -o bin/manager main.go 67 | 68 | .PHONY: run 69 | run: manifests generate fmt vet ## Run a controller from your host. 70 | go run ./main.go 71 | 72 | .PHONY: docker-build 73 | docker-build: test ## Build docker image with the manager. 74 | docker build --load -t ${IMG} . 75 | 76 | .PHONY: docker-push 77 | docker-push: ## Push docker image with the manager. 78 | docker push ${IMG} 79 | 80 | ##@ Deployment 81 | 82 | ifndef ignore-not-found 83 | ignore-not-found = false 84 | endif 85 | 86 | .PHONY: install 87 | install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. 88 | $(KUSTOMIZE) build config/crd | kubectl apply -f - 89 | 90 | .PHONY: uninstall 91 | uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. 92 | $(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f - 93 | 94 | .PHONY: deploy 95 | deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. 96 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 97 | $(KUSTOMIZE) build config/default | kubectl apply -f - 98 | 99 | .PHONY: undeploy 100 | undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. 101 | $(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f - 102 | 103 | ##@ Build Dependencies 104 | 105 | ## Location to install dependencies to 106 | LOCALBIN ?= $(shell pwd)/bin 107 | $(LOCALBIN): 108 | mkdir -p $(LOCALBIN) 109 | 110 | ## Tool Binaries 111 | KUSTOMIZE ?= $(LOCALBIN)/kustomize 112 | CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen 113 | ENVTEST ?= $(LOCALBIN)/setup-envtest 114 | 115 | ## Tool Versions 116 | KUSTOMIZE_VERSION ?= v4.5.5 117 | CONTROLLER_TOOLS_VERSION ?= v0.9.0 118 | 119 | .PHONY: kustomize 120 | kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. 121 | $(KUSTOMIZE): $(LOCALBIN) 122 | GOBIN=$(LOCALBIN) go install sigs.k8s.io/kustomize/kustomize/v4@$(KUSTOMIZE_VERSION) 123 | 124 | .PHONY: controller-gen 125 | controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. 126 | $(CONTROLLER_GEN): $(LOCALBIN) 127 | GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) 128 | 129 | .PHONY: envtest 130 | envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. 131 | $(ENVTEST): $(LOCALBIN) 132 | GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest 133 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: barpilot.io 2 | layout: 3 | - go.kubebuilder.io/v3 4 | projectName: headscale-operator 5 | repo: github.com/guilhem/headscale-operator 6 | resources: 7 | - api: 8 | crdVersion: v1 9 | namespaced: true 10 | controller: true 11 | domain: barpilot.io 12 | group: headscale 13 | kind: Server 14 | path: github.com/guilhem/headscale-operator/api/v1alpha1 15 | version: v1alpha1 16 | - api: 17 | crdVersion: v1 18 | namespaced: true 19 | controller: true 20 | domain: barpilot.io 21 | group: headscale 22 | kind: Namespace 23 | path: github.com/guilhem/headscale-operator/api/v1alpha1 24 | version: v1alpha1 25 | - api: 26 | crdVersion: v1 27 | namespaced: true 28 | controller: true 29 | domain: barpilot.io 30 | group: headscale 31 | kind: PreAuthKey 32 | path: github.com/guilhem/headscale-operator/api/v1alpha1 33 | version: v1alpha1 34 | version: "3" 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # headscale-operator 2 | 3 | A Kubernetes Operator to instantiate and control headscale instances. 4 | 5 | ## Description 6 | 7 | With _headscale-operator_ you can instantiate multiple [headscale](https://github.com/juanfont/headscale) servers for multiple purpose. 8 | 9 | _headscale-operator_ let you manage [_namespaces_](https://github.com/juanfont/headscale/blob/main/docs/glossary.md) and _preauthkeys_. 10 | 11 | ## Getting Started 12 | 13 | You’ll need a Kubernetes cluster to run against. You can use [KIND](https://sigs.k8s.io/kind) to get a local cluster for testing, or run against a remote cluster. 14 | **Note:** Your controller will automatically use the current context in your kubeconfig file (i.e. whatever cluster `kubectl cluster-info` shows). 15 | 16 | ### Running on the cluster 17 | 18 | 1. Install Instances of Custom Resources: 19 | 20 | ```sh 21 | kubectl apply -f config/crd/bases/ 22 | kubectl apply -f config/samples/ 23 | ``` 24 | 25 | 1. Build and push your image to the location specified by `IMG`: 26 | 27 | ```sh 28 | make docker-build docker-push IMG=/headscale-operator:tag 29 | ``` 30 | 31 | 1. Deploy the controller to the cluster with the image specified by `IMG`: 32 | 33 | ```sh 34 | make deploy IMG=/headscale-operator:tag 35 | ``` 36 | 37 | ### Uninstall CRDs 38 | 39 | To delete the CRDs from the cluster: 40 | 41 | ```sh 42 | make uninstall 43 | ``` 44 | 45 | ### Undeploy controller 46 | 47 | UnDeploy the controller to the cluster: 48 | 49 | ```sh 50 | make undeploy 51 | ``` 52 | 53 | ### How it works 54 | 55 | This project aims to follow the Kubernetes [Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/) 56 | 57 | It uses [Controllers](https://kubernetes.io/docs/concepts/architecture/controller/) 58 | which provides a reconcile function responsible for synchronizing resources untile the desired state is reached on the cluster 59 | 60 | ### Test It Out 61 | 62 | 1. Install the CRDs into the cluster: 63 | 64 | ```sh 65 | make install 66 | ``` 67 | 68 | 1. Run your controller (this will run in the foreground, so switch to a new terminal if you want to leave it running): 69 | 70 | ```sh 71 | make run 72 | ``` 73 | 74 | **NOTE:** You can also run this in one step by running: `make install run` 75 | 76 | ### Modifying the API definitions 77 | 78 | If you are editing the API definitions, generate the manifests such as CRs or CRDs using: 79 | 80 | ```sh 81 | make manifests 82 | ``` 83 | 84 | **NOTE:** Run `make --help` for more information on all potential `make` targets 85 | 86 | More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) 87 | 88 | ## License 89 | 90 | Copyright 2022 Guilhem Lettron. 91 | 92 | Licensed under the Apache License, Version 2.0 (the "License"); 93 | you may not use this file except in compliance with the License. 94 | You may obtain a copy of the License at 95 | 96 | http://www.apache.org/licenses/LICENSE-2.0 97 | 98 | Unless required by applicable law or agreed to in writing, software 99 | distributed under the License is distributed on an "AS IS" BASIS, 100 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 101 | See the License for the specific language governing permissions and 102 | limitations under the License. 103 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Guilhem Lettron. 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 headscale v1alpha1 API group 18 | //+kubebuilder:object:generate=true 19 | //+groupName=headscale.barpilot.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: "headscale.barpilot.io", Version: "v1alpha1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /api/v1alpha1/namespace_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Guilhem Lettron. 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 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // NamespaceSpec defines the desired state of Namespace 24 | type NamespaceSpec struct { 25 | Server string `json:"server"` 26 | } 27 | 28 | // NamespaceStatus defines the observed state of Namespace 29 | type NamespaceStatus struct { 30 | Created bool `json:"created"` 31 | } 32 | 33 | //+kubebuilder:object:root=true 34 | //+kubebuilder:subresource:status 35 | 36 | // Namespace is the Schema for the namespaces API 37 | type Namespace struct { 38 | metav1.TypeMeta `json:",inline"` 39 | metav1.ObjectMeta `json:"metadata,omitempty"` 40 | 41 | Spec NamespaceSpec `json:"spec,omitempty"` 42 | Status NamespaceStatus `json:"status,omitempty"` 43 | } 44 | 45 | //+kubebuilder:object:root=true 46 | 47 | // NamespaceList contains a list of Namespace 48 | type NamespaceList struct { 49 | metav1.TypeMeta `json:",inline"` 50 | metav1.ListMeta `json:"metadata,omitempty"` 51 | Items []Namespace `json:"items"` 52 | } 53 | 54 | func init() { 55 | SchemeBuilder.Register(&Namespace{}, &NamespaceList{}) 56 | } 57 | -------------------------------------------------------------------------------- /api/v1alpha1/preauthkey_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Guilhem Lettron. 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 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // PreAuthKeySpec defines the desired state of PreAuthKey 24 | type PreAuthKeySpec struct { 25 | Namespace string `json:"namespace"` 26 | Reusable bool `json:"reusable"` 27 | Ephemeral bool `json:"ephemeral"` 28 | Duration string `json:"duration"` 29 | } 30 | 31 | // PreAuthKeyStatus defines the observed state of PreAuthKey 32 | type PreAuthKeyStatus struct { 33 | Used bool `json:"used"` 34 | ID string `json:"id"` 35 | Expiration string `json:"expiration"` 36 | CreatedAt string `json:"createdAt"` 37 | Key string `json:"key"` 38 | } 39 | 40 | //+kubebuilder:object:root=true 41 | //+kubebuilder:subresource:status 42 | 43 | // PreAuthKey is the Schema for the preauthkeys API 44 | type PreAuthKey struct { 45 | metav1.TypeMeta `json:",inline"` 46 | metav1.ObjectMeta `json:"metadata,omitempty"` 47 | 48 | Spec PreAuthKeySpec `json:"spec,omitempty"` 49 | Status PreAuthKeyStatus `json:"status,omitempty"` 50 | } 51 | 52 | //+kubebuilder:object:root=true 53 | 54 | // PreAuthKeyList contains a list of PreAuthKey 55 | type PreAuthKeyList struct { 56 | metav1.TypeMeta `json:",inline"` 57 | metav1.ListMeta `json:"metadata,omitempty"` 58 | Items []PreAuthKey `json:"items"` 59 | } 60 | 61 | func init() { 62 | SchemeBuilder.Register(&PreAuthKey{}, &PreAuthKeyList{}) 63 | } 64 | -------------------------------------------------------------------------------- /api/v1alpha1/server_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Guilhem Lettron. 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 | "github.com/guilhem/headscale-operator/pkg/headscale" 21 | 22 | networkingv1 "k8s.io/api/networking/v1" 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | ) 25 | 26 | // ServerSpec defines the desired state of Server 27 | type ServerSpec struct { 28 | //+optional 29 | Version string `json:"version"` 30 | 31 | //+optional 32 | //+kubebuilder:default=false 33 | Debug bool `json:"debug"` 34 | 35 | //+optional 36 | Issuer string `json:"issuer,omitempty"` 37 | 38 | //+optional 39 | GrpcServiceName string `json:"grpcServiceName,omitempty"` 40 | 41 | // +kubebuilder:validation:Schemaless 42 | // +kubebuilder:pruning:PreserveUnknownFields 43 | Config headscale.Config `json:"config,omitempty"` 44 | 45 | // +kubebuilder:validation:Format=hostname 46 | // +kubebuilder:validation:Required 47 | Host string `json:"host,omitempty"` 48 | 49 | // +optional 50 | // +kubebuilder:validation:Schemaless 51 | // +kubebuilder:pruning:PreserveUnknownFields 52 | Ingress *networkingv1.Ingress `json:"ingress,omitempty"` 53 | } 54 | 55 | // ServerStatus defines the observed state of Server 56 | type ServerStatus struct { 57 | GrpcAddress string `json:"grpcAddress,omitempty"` 58 | 59 | DeploymentName string `json:"deploymentName,omitempty"` 60 | } 61 | 62 | //+kubebuilder:object:root=true 63 | //+kubebuilder:subresource:status 64 | 65 | // Server is the Schema for the servers API 66 | type Server struct { 67 | metav1.TypeMeta `json:",inline"` 68 | metav1.ObjectMeta `json:"metadata,omitempty"` 69 | 70 | Spec ServerSpec `json:"spec,omitempty"` 71 | Status ServerStatus `json:"status,omitempty"` 72 | } 73 | 74 | //+kubebuilder:object:root=true 75 | 76 | // ServerList contains a list of Server 77 | type ServerList struct { 78 | metav1.TypeMeta `json:",inline"` 79 | metav1.ListMeta `json:"metadata,omitempty"` 80 | Items []Server `json:"items"` 81 | } 82 | 83 | func init() { 84 | SchemeBuilder.Register(&Server{}, &ServerList{}) 85 | } 86 | -------------------------------------------------------------------------------- /api/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | /* 5 | Copyright 2022 Guilhem Lettron. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | // Code generated by controller-gen. DO NOT EDIT. 21 | 22 | package v1alpha1 23 | 24 | import ( 25 | "k8s.io/api/networking/v1" 26 | runtime "k8s.io/apimachinery/pkg/runtime" 27 | ) 28 | 29 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 30 | func (in *Namespace) DeepCopyInto(out *Namespace) { 31 | *out = *in 32 | out.TypeMeta = in.TypeMeta 33 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 34 | out.Spec = in.Spec 35 | out.Status = in.Status 36 | } 37 | 38 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Namespace. 39 | func (in *Namespace) DeepCopy() *Namespace { 40 | if in == nil { 41 | return nil 42 | } 43 | out := new(Namespace) 44 | in.DeepCopyInto(out) 45 | return out 46 | } 47 | 48 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 49 | func (in *Namespace) DeepCopyObject() runtime.Object { 50 | if c := in.DeepCopy(); c != nil { 51 | return c 52 | } 53 | return nil 54 | } 55 | 56 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 57 | func (in *NamespaceList) DeepCopyInto(out *NamespaceList) { 58 | *out = *in 59 | out.TypeMeta = in.TypeMeta 60 | in.ListMeta.DeepCopyInto(&out.ListMeta) 61 | if in.Items != nil { 62 | in, out := &in.Items, &out.Items 63 | *out = make([]Namespace, len(*in)) 64 | for i := range *in { 65 | (*in)[i].DeepCopyInto(&(*out)[i]) 66 | } 67 | } 68 | } 69 | 70 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceList. 71 | func (in *NamespaceList) DeepCopy() *NamespaceList { 72 | if in == nil { 73 | return nil 74 | } 75 | out := new(NamespaceList) 76 | in.DeepCopyInto(out) 77 | return out 78 | } 79 | 80 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 81 | func (in *NamespaceList) DeepCopyObject() runtime.Object { 82 | if c := in.DeepCopy(); c != nil { 83 | return c 84 | } 85 | return nil 86 | } 87 | 88 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 89 | func (in *NamespaceSpec) DeepCopyInto(out *NamespaceSpec) { 90 | *out = *in 91 | } 92 | 93 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceSpec. 94 | func (in *NamespaceSpec) DeepCopy() *NamespaceSpec { 95 | if in == nil { 96 | return nil 97 | } 98 | out := new(NamespaceSpec) 99 | in.DeepCopyInto(out) 100 | return out 101 | } 102 | 103 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 104 | func (in *NamespaceStatus) DeepCopyInto(out *NamespaceStatus) { 105 | *out = *in 106 | } 107 | 108 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceStatus. 109 | func (in *NamespaceStatus) DeepCopy() *NamespaceStatus { 110 | if in == nil { 111 | return nil 112 | } 113 | out := new(NamespaceStatus) 114 | in.DeepCopyInto(out) 115 | return out 116 | } 117 | 118 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 119 | func (in *PreAuthKey) DeepCopyInto(out *PreAuthKey) { 120 | *out = *in 121 | out.TypeMeta = in.TypeMeta 122 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 123 | out.Spec = in.Spec 124 | out.Status = in.Status 125 | } 126 | 127 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PreAuthKey. 128 | func (in *PreAuthKey) DeepCopy() *PreAuthKey { 129 | if in == nil { 130 | return nil 131 | } 132 | out := new(PreAuthKey) 133 | in.DeepCopyInto(out) 134 | return out 135 | } 136 | 137 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 138 | func (in *PreAuthKey) DeepCopyObject() runtime.Object { 139 | if c := in.DeepCopy(); c != nil { 140 | return c 141 | } 142 | return nil 143 | } 144 | 145 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 146 | func (in *PreAuthKeyList) DeepCopyInto(out *PreAuthKeyList) { 147 | *out = *in 148 | out.TypeMeta = in.TypeMeta 149 | in.ListMeta.DeepCopyInto(&out.ListMeta) 150 | if in.Items != nil { 151 | in, out := &in.Items, &out.Items 152 | *out = make([]PreAuthKey, len(*in)) 153 | for i := range *in { 154 | (*in)[i].DeepCopyInto(&(*out)[i]) 155 | } 156 | } 157 | } 158 | 159 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PreAuthKeyList. 160 | func (in *PreAuthKeyList) DeepCopy() *PreAuthKeyList { 161 | if in == nil { 162 | return nil 163 | } 164 | out := new(PreAuthKeyList) 165 | in.DeepCopyInto(out) 166 | return out 167 | } 168 | 169 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 170 | func (in *PreAuthKeyList) DeepCopyObject() runtime.Object { 171 | if c := in.DeepCopy(); c != nil { 172 | return c 173 | } 174 | return nil 175 | } 176 | 177 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 178 | func (in *PreAuthKeySpec) DeepCopyInto(out *PreAuthKeySpec) { 179 | *out = *in 180 | } 181 | 182 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PreAuthKeySpec. 183 | func (in *PreAuthKeySpec) DeepCopy() *PreAuthKeySpec { 184 | if in == nil { 185 | return nil 186 | } 187 | out := new(PreAuthKeySpec) 188 | in.DeepCopyInto(out) 189 | return out 190 | } 191 | 192 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 193 | func (in *PreAuthKeyStatus) DeepCopyInto(out *PreAuthKeyStatus) { 194 | *out = *in 195 | } 196 | 197 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PreAuthKeyStatus. 198 | func (in *PreAuthKeyStatus) DeepCopy() *PreAuthKeyStatus { 199 | if in == nil { 200 | return nil 201 | } 202 | out := new(PreAuthKeyStatus) 203 | in.DeepCopyInto(out) 204 | return out 205 | } 206 | 207 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 208 | func (in *Server) DeepCopyInto(out *Server) { 209 | *out = *in 210 | out.TypeMeta = in.TypeMeta 211 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 212 | in.Spec.DeepCopyInto(&out.Spec) 213 | out.Status = in.Status 214 | } 215 | 216 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Server. 217 | func (in *Server) DeepCopy() *Server { 218 | if in == nil { 219 | return nil 220 | } 221 | out := new(Server) 222 | in.DeepCopyInto(out) 223 | return out 224 | } 225 | 226 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 227 | func (in *Server) DeepCopyObject() runtime.Object { 228 | if c := in.DeepCopy(); c != nil { 229 | return c 230 | } 231 | return nil 232 | } 233 | 234 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 235 | func (in *ServerList) DeepCopyInto(out *ServerList) { 236 | *out = *in 237 | out.TypeMeta = in.TypeMeta 238 | in.ListMeta.DeepCopyInto(&out.ListMeta) 239 | if in.Items != nil { 240 | in, out := &in.Items, &out.Items 241 | *out = make([]Server, len(*in)) 242 | for i := range *in { 243 | (*in)[i].DeepCopyInto(&(*out)[i]) 244 | } 245 | } 246 | } 247 | 248 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerList. 249 | func (in *ServerList) DeepCopy() *ServerList { 250 | if in == nil { 251 | return nil 252 | } 253 | out := new(ServerList) 254 | in.DeepCopyInto(out) 255 | return out 256 | } 257 | 258 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 259 | func (in *ServerList) DeepCopyObject() runtime.Object { 260 | if c := in.DeepCopy(); c != nil { 261 | return c 262 | } 263 | return nil 264 | } 265 | 266 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 267 | func (in *ServerSpec) DeepCopyInto(out *ServerSpec) { 268 | *out = *in 269 | in.Config.DeepCopyInto(&out.Config) 270 | if in.Ingress != nil { 271 | in, out := &in.Ingress, &out.Ingress 272 | *out = new(v1.Ingress) 273 | (*in).DeepCopyInto(*out) 274 | } 275 | } 276 | 277 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerSpec. 278 | func (in *ServerSpec) DeepCopy() *ServerSpec { 279 | if in == nil { 280 | return nil 281 | } 282 | out := new(ServerSpec) 283 | in.DeepCopyInto(out) 284 | return out 285 | } 286 | 287 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 288 | func (in *ServerStatus) DeepCopyInto(out *ServerStatus) { 289 | *out = *in 290 | } 291 | 292 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerStatus. 293 | func (in *ServerStatus) DeepCopy() *ServerStatus { 294 | if in == nil { 295 | return nil 296 | } 297 | out := new(ServerStatus) 298 | in.DeepCopyInto(out) 299 | return out 300 | } 301 | -------------------------------------------------------------------------------- /config/crd/bases/headscale.barpilot.io_namespaces.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.9.0 7 | creationTimestamp: null 8 | name: namespaces.headscale.barpilot.io 9 | spec: 10 | group: headscale.barpilot.io 11 | names: 12 | kind: Namespace 13 | listKind: NamespaceList 14 | plural: namespaces 15 | singular: namespace 16 | scope: Namespaced 17 | versions: 18 | - name: v1alpha1 19 | schema: 20 | openAPIV3Schema: 21 | description: Namespace is the Schema for the namespaces API 22 | properties: 23 | apiVersion: 24 | description: 'APIVersion defines the versioned schema of this representation 25 | of an object. Servers should convert recognized schemas to the latest 26 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 27 | type: string 28 | kind: 29 | description: 'Kind is a string value representing the REST resource this 30 | object represents. Servers may infer this from the endpoint the client 31 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 32 | type: string 33 | metadata: 34 | type: object 35 | spec: 36 | description: NamespaceSpec defines the desired state of Namespace 37 | properties: 38 | server: 39 | type: string 40 | required: 41 | - server 42 | type: object 43 | status: 44 | description: NamespaceStatus defines the observed state of Namespace 45 | properties: 46 | created: 47 | type: boolean 48 | required: 49 | - created 50 | type: object 51 | type: object 52 | served: true 53 | storage: true 54 | subresources: 55 | status: {} 56 | -------------------------------------------------------------------------------- /config/crd/bases/headscale.barpilot.io_preauthkeys.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.9.0 7 | creationTimestamp: null 8 | name: preauthkeys.headscale.barpilot.io 9 | spec: 10 | group: headscale.barpilot.io 11 | names: 12 | kind: PreAuthKey 13 | listKind: PreAuthKeyList 14 | plural: preauthkeys 15 | singular: preauthkey 16 | scope: Namespaced 17 | versions: 18 | - name: v1alpha1 19 | schema: 20 | openAPIV3Schema: 21 | description: PreAuthKey is the Schema for the preauthkeys API 22 | properties: 23 | apiVersion: 24 | description: 'APIVersion defines the versioned schema of this representation 25 | of an object. Servers should convert recognized schemas to the latest 26 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 27 | type: string 28 | kind: 29 | description: 'Kind is a string value representing the REST resource this 30 | object represents. Servers may infer this from the endpoint the client 31 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 32 | type: string 33 | metadata: 34 | type: object 35 | spec: 36 | description: PreAuthKeySpec defines the desired state of PreAuthKey 37 | properties: 38 | duration: 39 | type: string 40 | ephemeral: 41 | type: boolean 42 | namespace: 43 | type: string 44 | reusable: 45 | type: boolean 46 | required: 47 | - duration 48 | - ephemeral 49 | - namespace 50 | - reusable 51 | type: object 52 | status: 53 | description: PreAuthKeyStatus defines the observed state of PreAuthKey 54 | properties: 55 | createdAt: 56 | type: string 57 | expiration: 58 | type: string 59 | id: 60 | type: string 61 | key: 62 | type: string 63 | used: 64 | type: boolean 65 | required: 66 | - createdAt 67 | - expiration 68 | - id 69 | - key 70 | - used 71 | type: object 72 | type: object 73 | served: true 74 | storage: true 75 | subresources: 76 | status: {} 77 | -------------------------------------------------------------------------------- /config/crd/bases/headscale.barpilot.io_servers.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.9.0 7 | creationTimestamp: null 8 | name: servers.headscale.barpilot.io 9 | spec: 10 | group: headscale.barpilot.io 11 | names: 12 | kind: Server 13 | listKind: ServerList 14 | plural: servers 15 | singular: server 16 | scope: Namespaced 17 | versions: 18 | - name: v1alpha1 19 | schema: 20 | openAPIV3Schema: 21 | description: Server is the Schema for the servers API 22 | properties: 23 | apiVersion: 24 | description: 'APIVersion defines the versioned schema of this representation 25 | of an object. Servers should convert recognized schemas to the latest 26 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 27 | type: string 28 | kind: 29 | description: 'Kind is a string value representing the REST resource this 30 | object represents. Servers may infer this from the endpoint the client 31 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 32 | type: string 33 | metadata: 34 | type: object 35 | spec: 36 | description: ServerSpec defines the desired state of Server 37 | properties: 38 | config: 39 | x-kubernetes-preserve-unknown-fields: true 40 | debug: 41 | default: false 42 | type: boolean 43 | grpcServiceName: 44 | type: string 45 | host: 46 | format: hostname 47 | type: string 48 | ingress: 49 | x-kubernetes-preserve-unknown-fields: true 50 | issuer: 51 | type: string 52 | version: 53 | type: string 54 | type: object 55 | status: 56 | description: ServerStatus defines the observed state of Server 57 | properties: 58 | deploymentName: 59 | type: string 60 | grpcAddress: 61 | type: string 62 | type: object 63 | type: object 64 | served: true 65 | storage: true 66 | subresources: 67 | status: {} 68 | -------------------------------------------------------------------------------- /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/headscale.barpilot.io_servers.yaml 6 | - bases/headscale.barpilot.io_namespaces.yaml 7 | - bases/headscale.barpilot.io_preauthkeys.yaml 8 | #+kubebuilder:scaffold:crdkustomizeresource 9 | 10 | patchesStrategicMerge: 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 | #- patches/webhook_in_servers.yaml 14 | #- patches/webhook_in_namespaces.yaml 15 | #- patches/webhook_in_preauthkeys.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 | #- patches/cainjection_in_servers.yaml 21 | #- patches/cainjection_in_namespaces.yaml 22 | #- patches/cainjection_in_preauthkeys.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_namespaces.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: namespaces.headscale.barpilot.io 8 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_preauthkeys.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: preauthkeys.headscale.barpilot.io 8 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_servers.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: servers.headscale.barpilot.io 8 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_namespaces.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: namespaces.headscale.barpilot.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_preauthkeys.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: preauthkeys.headscale.barpilot.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_servers.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: servers.headscale.barpilot.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: headscale-operator-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: headscale-operator- 10 | 11 | # Labels to add to all resources and selectors. 12 | #commonLabels: 13 | # someName: someValue 14 | 15 | bases: 16 | - ../crd 17 | - ../rbac 18 | - ../manager 19 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 20 | # crd/kustomization.yaml 21 | #- ../webhook 22 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 23 | #- ../certmanager 24 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 25 | #- ../prometheus 26 | 27 | patchesStrategicMerge: 28 | # Protect the /metrics endpoint by putting it behind auth. 29 | # If you want your controller-manager to expose the /metrics 30 | # endpoint w/o any authn/z, please comment the following line. 31 | - manager_auth_proxy_patch.yaml 32 | 33 | # Mount the controller config file for loading manager configurations 34 | # through a ComponentConfig type 35 | #- manager_config_patch.yaml 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 | # the following config is for teaching kustomize how to do var substitution 47 | vars: 48 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 49 | #- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR 50 | # objref: 51 | # kind: Certificate 52 | # group: cert-manager.io 53 | # version: v1 54 | # name: serving-cert # this name should match the one in certificate.yaml 55 | # fieldref: 56 | # fieldpath: metadata.namespace 57 | #- name: CERTIFICATE_NAME 58 | # objref: 59 | # kind: Certificate 60 | # group: cert-manager.io 61 | # version: v1 62 | # name: serving-cert # this name should match the one in certificate.yaml 63 | #- name: SERVICE_NAMESPACE # namespace of the service 64 | # objref: 65 | # kind: Service 66 | # version: v1 67 | # name: webhook-service 68 | # fieldref: 69 | # fieldpath: metadata.namespace 70 | #- name: SERVICE_NAME 71 | # objref: 72 | # kind: Service 73 | # version: v1 74 | # name: webhook-service 75 | -------------------------------------------------------------------------------- /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 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.11.0 14 | args: 15 | - "--secure-listen-address=0.0.0.0:8443" 16 | - "--upstream=http://127.0.0.1:8080/" 17 | - "--logtostderr=true" 18 | - "--v=0" 19 | ports: 20 | - containerPort: 8443 21 | protocol: TCP 22 | name: https 23 | resources: 24 | limits: 25 | cpu: 500m 26 | memory: 128Mi 27 | requests: 28 | cpu: 5m 29 | memory: 64Mi 30 | - name: manager 31 | args: 32 | - "--health-probe-bind-address=:8081" 33 | - "--metrics-bind-address=127.0.0.1:8080" 34 | - "--leader-elect" 35 | -------------------------------------------------------------------------------- /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 | args: 12 | - "--config=controller_manager_config.yaml" 13 | volumeMounts: 14 | - name: manager-config 15 | mountPath: /controller_manager_config.yaml 16 | subPath: controller_manager_config.yaml 17 | volumes: 18 | - name: manager-config 19 | configMap: 20 | name: manager-config 21 | -------------------------------------------------------------------------------- /config/manager/controller_manager_config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 2 | kind: ControllerManagerConfig 3 | health: 4 | healthProbeBindAddress: :8081 5 | metrics: 6 | bindAddress: 127.0.0.1:8080 7 | webhook: 8 | port: 9443 9 | leaderElection: 10 | leaderElect: true 11 | resourceName: 67c702f3.barpilot.io 12 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | 4 | generatorOptions: 5 | disableNameSuffixHash: true 6 | 7 | configMapGenerator: 8 | - files: 9 | - controller_manager_config.yaml 10 | name: manager-config 11 | apiVersion: kustomize.config.k8s.io/v1beta1 12 | kind: Kustomization 13 | images: 14 | - name: controller 15 | newName: europe-west1-docker.pkg.dev/themecloud-dev/test/headscale-operator 16 | newTag: test1 17 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: system 7 | --- 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | metadata: 11 | name: controller-manager 12 | namespace: system 13 | labels: 14 | control-plane: controller-manager 15 | spec: 16 | selector: 17 | matchLabels: 18 | control-plane: controller-manager 19 | replicas: 1 20 | template: 21 | metadata: 22 | annotations: 23 | kubectl.kubernetes.io/default-container: manager 24 | labels: 25 | control-plane: controller-manager 26 | spec: 27 | securityContext: 28 | runAsNonRoot: true 29 | containers: 30 | - command: 31 | - /manager 32 | args: 33 | - --leader-elect 34 | image: controller:latest 35 | imagePullPolicy: Always 36 | name: manager 37 | securityContext: 38 | allowPrivilegeEscalation: false 39 | livenessProbe: 40 | httpGet: 41 | path: /healthz 42 | port: 8081 43 | initialDelaySeconds: 15 44 | periodSeconds: 20 45 | readinessProbe: 46 | httpGet: 47 | path: /readyz 48 | port: 8081 49 | initialDelaySeconds: 5 50 | periodSeconds: 10 51 | # TODO(user): Configure the resources accordingly based on the project requirements. 52 | # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 53 | resources: 54 | limits: 55 | cpu: 500m 56 | memory: 128Mi 57 | requests: 58 | cpu: 10m 59 | memory: 64Mi 60 | serviceAccountName: controller-manager 61 | terminationGracePeriodSeconds: 10 62 | -------------------------------------------------------------------------------- /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 | name: controller-manager-metrics-monitor 9 | namespace: system 10 | spec: 11 | endpoints: 12 | - path: /metrics 13 | port: https 14 | scheme: https 15 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 16 | tlsConfig: 17 | insecureSkipVerify: true 18 | selector: 19 | matchLabels: 20 | control-plane: controller-manager 21 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-reader 5 | rules: 6 | - nonResourceURLs: 7 | - "/metrics" 8 | verbs: 9 | - get 10 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: proxy-role 5 | rules: 6 | - apiGroups: 7 | - authentication.k8s.io 8 | resources: 9 | - tokenreviews 10 | verbs: 11 | - create 12 | - apiGroups: 13 | - authorization.k8s.io 14 | resources: 15 | - subjectaccessreviews 16 | verbs: 17 | - create 18 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: proxy-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: proxy-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: controller-manager-metrics-service 7 | namespace: system 8 | spec: 9 | ports: 10 | - name: https 11 | port: 8443 12 | protocol: TCP 13 | targetPort: https 14 | selector: 15 | control-plane: controller-manager 16 | -------------------------------------------------------------------------------- /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 | name: leader-election-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - create 16 | - update 17 | - patch 18 | - delete 19 | - apiGroups: 20 | - coordination.k8s.io 21 | resources: 22 | - leases 23 | verbs: 24 | - get 25 | - list 26 | - watch 27 | - create 28 | - update 29 | - patch 30 | - delete 31 | - apiGroups: 32 | - "" 33 | resources: 34 | - events 35 | verbs: 36 | - create 37 | - patch 38 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: leader-election-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: leader-election-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/namespace_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit namespaces. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: namespace-editor-role 6 | rules: 7 | - apiGroups: 8 | - headscale.barpilot.io 9 | resources: 10 | - namespaces 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - headscale.barpilot.io 21 | resources: 22 | - namespaces/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/namespace_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view namespaces. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: namespace-viewer-role 6 | rules: 7 | - apiGroups: 8 | - headscale.barpilot.io 9 | resources: 10 | - namespaces 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - headscale.barpilot.io 17 | resources: 18 | - namespaces/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/preauthkey_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit preauthkeys. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: preauthkey-editor-role 6 | rules: 7 | - apiGroups: 8 | - headscale.barpilot.io 9 | resources: 10 | - preauthkeys 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - headscale.barpilot.io 21 | resources: 22 | - preauthkeys/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/preauthkey_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view preauthkeys. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: preauthkey-viewer-role 6 | rules: 7 | - apiGroups: 8 | - headscale.barpilot.io 9 | resources: 10 | - preauthkeys 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - headscale.barpilot.io 17 | resources: 18 | - preauthkeys/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | creationTimestamp: null 6 | name: manager-role 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - events 12 | verbs: 13 | - create 14 | - patch 15 | - apiGroups: 16 | - apps 17 | resources: 18 | - deployments 19 | verbs: 20 | - create 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - cert-manager.io 28 | resources: 29 | - certificates 30 | verbs: 31 | - create 32 | - get 33 | - list 34 | - patch 35 | - update 36 | - watch 37 | - apiGroups: 38 | - "" 39 | resources: 40 | - configmaps 41 | verbs: 42 | - create 43 | - get 44 | - list 45 | - patch 46 | - update 47 | - watch 48 | - apiGroups: 49 | - "" 50 | resources: 51 | - services 52 | verbs: 53 | - create 54 | - get 55 | - list 56 | - patch 57 | - update 58 | - watch 59 | - apiGroups: 60 | - headscale.barpilot.io 61 | resources: 62 | - namespaces 63 | verbs: 64 | - create 65 | - delete 66 | - get 67 | - list 68 | - patch 69 | - update 70 | - watch 71 | - apiGroups: 72 | - headscale.barpilot.io 73 | resources: 74 | - namespaces/finalizers 75 | verbs: 76 | - update 77 | - apiGroups: 78 | - headscale.barpilot.io 79 | resources: 80 | - namespaces/status 81 | verbs: 82 | - get 83 | - patch 84 | - update 85 | - apiGroups: 86 | - headscale.barpilot.io 87 | resources: 88 | - preauthkeys 89 | verbs: 90 | - create 91 | - delete 92 | - get 93 | - list 94 | - patch 95 | - update 96 | - watch 97 | - apiGroups: 98 | - headscale.barpilot.io 99 | resources: 100 | - preauthkeys/finalizers 101 | verbs: 102 | - update 103 | - apiGroups: 104 | - headscale.barpilot.io 105 | resources: 106 | - preauthkeys/status 107 | verbs: 108 | - get 109 | - patch 110 | - update 111 | - apiGroups: 112 | - headscale.barpilot.io 113 | resources: 114 | - servers 115 | verbs: 116 | - get 117 | - list 118 | - patch 119 | - update 120 | - watch 121 | - apiGroups: 122 | - headscale.barpilot.io 123 | resources: 124 | - servers/finalizers 125 | verbs: 126 | - update 127 | - apiGroups: 128 | - headscale.barpilot.io 129 | resources: 130 | - servers/status 131 | verbs: 132 | - get 133 | - patch 134 | - update 135 | - apiGroups: 136 | - networking.k8s.io 137 | resources: 138 | - ingresses 139 | verbs: 140 | - create 141 | - get 142 | - list 143 | - patch 144 | - update 145 | - watch 146 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: manager-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: manager-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/server_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit servers. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: server-editor-role 6 | rules: 7 | - apiGroups: 8 | - headscale.barpilot.io 9 | resources: 10 | - servers 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - headscale.barpilot.io 21 | resources: 22 | - servers/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/server_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view servers. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: server-viewer-role 6 | rules: 7 | - apiGroups: 8 | - headscale.barpilot.io 9 | resources: 10 | - servers 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - headscale.barpilot.io 17 | resources: 18 | - servers/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | -------------------------------------------------------------------------------- /config/samples/assests/issuer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1 2 | kind: Issuer 3 | metadata: 4 | name: selfsigned 5 | spec: 6 | selfSigned: {} 7 | -------------------------------------------------------------------------------- /config/samples/headscale_v1alpha1_namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: headscale.barpilot.io/v1alpha1 2 | kind: Namespace 3 | metadata: 4 | name: namespace-sample 5 | spec: 6 | server: server-sample 7 | -------------------------------------------------------------------------------- /config/samples/headscale_v1alpha1_preauthkey.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: headscale.barpilot.io/v1alpha1 2 | kind: PreAuthKey 3 | metadata: 4 | name: preauthkey-sample 5 | spec: 6 | namespace: namespace-sample 7 | reusable: true 8 | ephemeral: true 9 | duration: 1h 10 | -------------------------------------------------------------------------------- /config/samples/headscale_v1alpha1_server.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: headscale.barpilot.io/v1alpha1 2 | kind: Server 3 | metadata: 4 | name: server-sample 5 | spec: 6 | version: 0.15.0 7 | issuer: selfsigned 8 | -------------------------------------------------------------------------------- /controllers/default.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/guilhem/headscale-operator/pkg/headscale" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/utils/pointer" 9 | ) 10 | 11 | var defaultServerConfig = headscale.Config{ 12 | Addr: "0.0.0.0:8080", 13 | MetricsAddr: "0.0.0.0:8081", 14 | // GRPCAddr: "0.0.0.0:8081", 15 | DERP: headscale.DERPConfig{ 16 | Server: headscale.DERPConfigServer{ 17 | Enabled: pointer.Bool(false), 18 | RegionID: 999, 19 | RegionCode: "headscale", 20 | RegionName: "Headscale Embedded DERP", 21 | STUNAddr: "0.0.0.0:3478", 22 | }, 23 | URLs: []string{"https://controlplane.tailscale.com/derpmap/default"}, 24 | AutoUpdate: pointer.Bool(true), 25 | Paths: []string{}, 26 | UpdateFrequency: metav1.Duration{Duration: time.Hour * 1}, 27 | }, 28 | EphemeralNodeInactivityTimeout: metav1.Duration{Duration: time.Hour * 24}, 29 | // ACMEURL: "https://acme-v02.api.letsencrypt.org/directory", 30 | // ACMEEmail: "", 31 | DNSConfig: headscale.DNSConfig{ 32 | Nameservers: []string{"1.1.1.1"}, 33 | Magic: pointer.Bool(true), 34 | Domains: []string{}, 35 | BaseDomain: "", 36 | }, 37 | LogLevel: "info", 38 | } 39 | -------------------------------------------------------------------------------- /controllers/namespace_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Guilhem Lettron. 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 controllers 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | "time" 23 | 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | ctrl "sigs.k8s.io/controller-runtime" 27 | "sigs.k8s.io/controller-runtime/pkg/client" 28 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 29 | "sigs.k8s.io/controller-runtime/pkg/log" 30 | 31 | headscalev1alpha1 "github.com/guilhem/headscale-operator/api/v1alpha1" 32 | "github.com/guilhem/headscale-operator/pkg/utils" 33 | v1 "github.com/juanfont/headscale/gen/go/headscale/v1" 34 | ) 35 | 36 | // NamespaceReconciler reconciles a Namespace object 37 | type NamespaceReconciler struct { 38 | client.Client 39 | Scheme *runtime.Scheme 40 | } 41 | 42 | //+kubebuilder:rbac:groups=headscale.barpilot.io,resources=namespaces,verbs=get;list;watch;create;update;patch;delete 43 | //+kubebuilder:rbac:groups=headscale.barpilot.io,resources=namespaces/status,verbs=get;update;patch 44 | //+kubebuilder:rbac:groups=headscale.barpilot.io,resources=namespaces/finalizers,verbs=update 45 | 46 | // Reconcile is part of the main kubernetes reconciliation loop which aims to 47 | // move the current state of the cluster closer to the desired state. 48 | // TODO(user): Modify the Reconcile function to compare the state specified by 49 | // the Namespace object against the actual cluster state, and then 50 | // perform operations to make the cluster state reflect the state specified by 51 | // the user. 52 | // 53 | // For more details, check Reconcile and its Result here: 54 | // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.2/pkg/reconcile 55 | func (r *NamespaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 56 | log := log.FromContext(ctx) 57 | 58 | instance := new(headscalev1alpha1.Namespace) 59 | if err := r.Get(ctx, req.NamespacedName, instance); err != nil { 60 | log.Error(err, "unable to fetch Namespace") 61 | // we'll ignore not-found errors, since they can't be fixed by an immediate 62 | // requeue (we'll need to wait for a new notification), and we can get them 63 | // on deleted requests. 64 | return ctrl.Result{}, client.IgnoreNotFound(err) 65 | } 66 | 67 | server := &headscalev1alpha1.Server{ 68 | ObjectMeta: metav1.ObjectMeta{ 69 | Name: instance.Spec.Server, 70 | Namespace: instance.Namespace, 71 | }, 72 | } 73 | 74 | if err := r.Get(ctx, client.ObjectKeyFromObject(server), server); err != nil { 75 | log.Error(err, "unable to fetch Server") 76 | 77 | return ctrl.Result{}, err 78 | } 79 | 80 | if server.Status.GrpcAddress == "" { 81 | err := errors.New("Server not ready") 82 | log.Error(err, "GrpcAddress empty") 83 | 84 | return ctrl.Result{}, err 85 | } 86 | 87 | clientCtx, cancel := context.WithTimeout(ctx, time.Second*10) 88 | defer cancel() 89 | 90 | client, err := utils.NewHeadscaleServiceClient(clientCtx, server.Status.GrpcAddress) 91 | if err != nil { 92 | return ctrl.Result{}, err 93 | } 94 | 95 | log.Info("Headscale Client created", "GrpcAddress", server.Status.GrpcAddress) 96 | 97 | // examine DeletionTimestamp to determine if object is under deletion 98 | if instance.ObjectMeta.DeletionTimestamp.IsZero() { 99 | // The object is not being deleted, so if it does not have our finalizer, 100 | // then lets add the finalizer and update the object. This is equivalent 101 | // registering our finalizer. 102 | if !controllerutil.ContainsFinalizer(instance, Finalizer) { 103 | controllerutil.AddFinalizer(instance, Finalizer) 104 | if err := r.Update(ctx, instance); err != nil { 105 | return ctrl.Result{}, err 106 | } 107 | } 108 | } else { 109 | // The object is being deleted 110 | if controllerutil.ContainsFinalizer(instance, Finalizer) { 111 | 112 | if _, err := client.DeleteNamespace(ctx, &v1.DeleteNamespaceRequest{Name: instance.Name}); utils.IgnoreNotFound(err) != nil { 113 | return ctrl.Result{}, err 114 | } 115 | 116 | // remove our finalizer from the list and update it. 117 | controllerutil.RemoveFinalizer(instance, Finalizer) 118 | if err := r.Update(ctx, instance); err != nil { 119 | return ctrl.Result{}, err 120 | } 121 | } 122 | 123 | // Stop reconciliation as the item is being deleted 124 | return ctrl.Result{}, nil 125 | } 126 | 127 | logNamespace := log.WithValues("namespace", instance.Name) 128 | if _, err := client.GetNamespace(ctx, &v1.GetNamespaceRequest{Name: instance.Name}); err != nil { 129 | 130 | if utils.IgnoreNotFound(err) != nil { 131 | logNamespace.Error(err, "Can't get Namespace") 132 | return ctrl.Result{}, err 133 | } 134 | 135 | if _, err := client.CreateNamespace(ctx, &v1.CreateNamespaceRequest{Name: instance.Name}); err != nil { 136 | logNamespace.Error(err, "can't create Namespace") 137 | return ctrl.Result{}, err 138 | } 139 | logNamespace.Info("Namespace Created") 140 | } else { 141 | logNamespace.Info("Namespace Already Exists") 142 | } 143 | 144 | instance.Status.Created = true 145 | 146 | if err := r.Status().Update(ctx, instance); err != nil { 147 | return ctrl.Result{}, err 148 | } 149 | 150 | // reconcile every 30s 151 | return ctrl.Result{RequeueAfter: time.Second * 30}, nil 152 | } 153 | 154 | // SetupWithManager sets up the controller with the Manager. 155 | func (r *NamespaceReconciler) SetupWithManager(mgr ctrl.Manager) error { 156 | return ctrl.NewControllerManagedBy(mgr). 157 | For(&headscalev1alpha1.Namespace{}). 158 | Complete(r) 159 | } 160 | -------------------------------------------------------------------------------- /controllers/preauthkey_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Guilhem Lettron. 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 controllers 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | "fmt" 23 | "time" 24 | 25 | "golang.org/x/exp/slices" 26 | "google.golang.org/protobuf/types/known/timestamppb" 27 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 | "k8s.io/apimachinery/pkg/runtime" 29 | "k8s.io/client-go/tools/record" 30 | ctrl "sigs.k8s.io/controller-runtime" 31 | "sigs.k8s.io/controller-runtime/pkg/client" 32 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 33 | "sigs.k8s.io/controller-runtime/pkg/log" 34 | 35 | headscalev1alpha1 "github.com/guilhem/headscale-operator/api/v1alpha1" 36 | "github.com/guilhem/headscale-operator/pkg/utils" 37 | v1 "github.com/juanfont/headscale/gen/go/headscale/v1" 38 | ) 39 | 40 | // PreAuthKeyReconciler reconciles a PreAuthKey object 41 | type PreAuthKeyReconciler struct { 42 | client.Client 43 | Scheme *runtime.Scheme 44 | recorder record.EventRecorder 45 | } 46 | 47 | //+kubebuilder:rbac:groups=headscale.barpilot.io,resources=preauthkeys,verbs=get;list;watch;create;update;patch;delete 48 | //+kubebuilder:rbac:groups=headscale.barpilot.io,resources=preauthkeys/status,verbs=get;update;patch 49 | //+kubebuilder:rbac:groups=headscale.barpilot.io,resources=preauthkeys/finalizers,verbs=update 50 | //+kubebuilder:rbac:groups="",resources=events,verbs=create;patch 51 | 52 | // Reconcile is part of the main kubernetes reconciliation loop which aims to 53 | // move the current state of the cluster closer to the desired state. 54 | // 55 | // For more details, check Reconcile and its Result here: 56 | // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.2/pkg/reconcile 57 | func (r *PreAuthKeyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 58 | log := log.FromContext(ctx) 59 | 60 | log.Info("Begin") 61 | 62 | instance := new(headscalev1alpha1.PreAuthKey) 63 | if err := r.Get(ctx, req.NamespacedName, instance); err != nil { 64 | log.Error(err, "unable to fetch PreAuthKey") 65 | // we'll ignore not-found errors, since they can't be fixed by an immediate 66 | // requeue (we'll need to wait for a new notification), and we can get them 67 | // on deleted requests. 68 | return ctrl.Result{}, client.IgnoreNotFound(err) 69 | } 70 | 71 | namespace := &headscalev1alpha1.Namespace{ 72 | ObjectMeta: metav1.ObjectMeta{ 73 | Name: instance.Spec.Namespace, 74 | Namespace: instance.Namespace, 75 | }, 76 | } 77 | 78 | if err := r.Get(ctx, client.ObjectKeyFromObject(namespace), namespace); err != nil { 79 | log.Error(err, "unable to fetch Namespace") 80 | 81 | return ctrl.Result{}, err 82 | } 83 | 84 | if !namespace.Status.Created { 85 | return ctrl.Result{}, errors.New("Namespace not created") 86 | } 87 | 88 | server := &headscalev1alpha1.Server{ 89 | ObjectMeta: metav1.ObjectMeta{ 90 | Name: namespace.Spec.Server, 91 | Namespace: instance.Namespace, 92 | }, 93 | } 94 | 95 | if err := r.Get(ctx, client.ObjectKeyFromObject(server), server); err != nil { 96 | log.Error(err, "unable to fetch Server") 97 | 98 | return ctrl.Result{}, err 99 | } 100 | 101 | if server.Status.GrpcAddress == "" { 102 | log.Info("Server not ready") 103 | 104 | return ctrl.Result{}, errors.New("Server not ready") 105 | } 106 | 107 | clientCtx, cancel := context.WithTimeout(ctx, time.Second*10) 108 | defer cancel() 109 | 110 | client, err := utils.NewHeadscaleServiceClient(clientCtx, server.Status.GrpcAddress) 111 | if err != nil { 112 | return ctrl.Result{}, err 113 | } 114 | 115 | // examine DeletionTimestamp to determine if object is under deletion 116 | if instance.ObjectMeta.DeletionTimestamp.IsZero() { 117 | // The object is not being deleted, so if it does not have our finalizer, 118 | // then lets add the finalizer and update the object. This is equivalent 119 | // registering our finalizer. 120 | if !controllerutil.ContainsFinalizer(instance, Finalizer) { 121 | controllerutil.AddFinalizer(instance, Finalizer) 122 | if err := r.Update(ctx, instance); err != nil { 123 | return ctrl.Result{}, err 124 | } 125 | } 126 | } else { 127 | // The object is being deleted 128 | if controllerutil.ContainsFinalizer(instance, Finalizer) { 129 | 130 | if _, err := client.ExpirePreAuthKey(ctx, &v1.ExpirePreAuthKeyRequest{Key: instance.Name, Namespace: namespace.Name}); utils.IgnoreNotFound(err) != nil { 131 | return ctrl.Result{}, err 132 | } 133 | 134 | // remove our finalizer from the list and update it. 135 | controllerutil.RemoveFinalizer(instance, Finalizer) 136 | if err := r.Update(ctx, instance); err != nil { 137 | return ctrl.Result{}, err 138 | } 139 | } 140 | 141 | // Stop reconciliation as the item is being deleted 142 | return ctrl.Result{}, nil 143 | } 144 | 145 | list, err := client.ListPreAuthKeys(ctx, &v1.ListPreAuthKeysRequest{Namespace: namespace.Name}) 146 | if err != nil { 147 | return ctrl.Result{}, err 148 | } 149 | 150 | preAuthKey := new(v1.PreAuthKey) 151 | 152 | if i := slices.IndexFunc(list.PreAuthKeys, func(pak *v1.PreAuthKey) bool { return pak.Namespace == namespace.Name }); i >= 0 { 153 | // PreAuthKey already exist 154 | preAuthKey = list.PreAuthKeys[i] 155 | 156 | } else { 157 | createPreAuthKeyRequest := &v1.CreatePreAuthKeyRequest{ 158 | Namespace: namespace.Name, 159 | Reusable: instance.Spec.Reusable, 160 | Ephemeral: instance.Spec.Ephemeral, 161 | } 162 | 163 | if d := instance.Spec.Duration; d != "" { 164 | duration, err := time.ParseDuration(d) 165 | if err != nil { 166 | return ctrl.Result{}, err 167 | } 168 | createPreAuthKeyRequest.Expiration = timestamppb.New(time.Now().Add(duration)) 169 | } 170 | 171 | createApiKeyResponse, err := client.CreatePreAuthKey(ctx, createPreAuthKeyRequest) 172 | if err != nil { 173 | return ctrl.Result{}, err 174 | } 175 | r.recorder.Event(instance, "Normal", "Created", fmt.Sprintf("PreAuthKey %s created", preAuthKey.Key)) 176 | 177 | preAuthKey = createApiKeyResponse.PreAuthKey 178 | } 179 | 180 | instance.Status.Used = preAuthKey.GetUsed() 181 | instance.Status.ID = preAuthKey.GetId() 182 | instance.Status.CreatedAt = preAuthKey.GetCreatedAt().AsTime().String() 183 | instance.Status.Expiration = preAuthKey.GetExpiration().AsTime().String() 184 | instance.Status.Key = preAuthKey.GetKey() 185 | 186 | if err := r.Status().Update(ctx, instance); err != nil { 187 | return ctrl.Result{}, err 188 | } 189 | 190 | // check every 30s 191 | return ctrl.Result{RequeueAfter: time.Second * 30}, nil 192 | } 193 | 194 | // SetupWithManager sets up the controller with the Manager. 195 | func (r *PreAuthKeyReconciler) SetupWithManager(mgr ctrl.Manager) error { 196 | r.recorder = mgr.GetEventRecorderFor("preauthkey-controller") 197 | 198 | return ctrl.NewControllerManagedBy(mgr). 199 | For(&headscalev1alpha1.PreAuthKey{}). 200 | Complete(r) 201 | } 202 | -------------------------------------------------------------------------------- /controllers/server_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Guilhem Lettron. 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 controllers 18 | 19 | import ( 20 | "context" 21 | "encoding/json" 22 | "fmt" 23 | "net" 24 | "path" 25 | "path/filepath" 26 | "strconv" 27 | 28 | "github.com/imdario/mergo" 29 | appsv1 "k8s.io/api/apps/v1" 30 | corev1 "k8s.io/api/core/v1" 31 | networkingv1 "k8s.io/api/networking/v1" 32 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 | "k8s.io/apimachinery/pkg/labels" 34 | "k8s.io/apimachinery/pkg/runtime" 35 | "k8s.io/apimachinery/pkg/util/intstr" 36 | "k8s.io/client-go/tools/record" 37 | "k8s.io/utils/pointer" 38 | ctrl "sigs.k8s.io/controller-runtime" 39 | "sigs.k8s.io/controller-runtime/pkg/client" 40 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 41 | "sigs.k8s.io/controller-runtime/pkg/log" 42 | 43 | headscalev1alpha1 "github.com/guilhem/headscale-operator/api/v1alpha1" 44 | "github.com/guilhem/headscale-operator/pkg/utils" 45 | ) 46 | 47 | // ServerReconciler reconciles a Server object 48 | type ServerReconciler struct { 49 | client.Client 50 | Scheme *runtime.Scheme 51 | recorder record.EventRecorder 52 | } 53 | 54 | const Finalizer = "headscale.barpilot.io/finalizer" 55 | 56 | const ConfigFileName = "config.json" 57 | 58 | // +kubebuilder:rbac:groups=headscale.barpilot.io,resources=servers,verbs=get;list;watch;update;patch 59 | // +kubebuilder:rbac:groups=headscale.barpilot.io,resources=servers/status,verbs=get;update;patch 60 | // +kubebuilder:rbac:groups=headscale.barpilot.io,resources=servers/finalizers,verbs=update 61 | // +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch 62 | // +kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch;create;update;patch 63 | // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch 64 | // +kubebuilder:rbac:groups=cert-manager.io,resources=certificates,verbs=get;list;watch;create;update;patch 65 | // +kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=get;list;watch;create;update;patch 66 | 67 | // Reconcile is part of the main kubernetes reconciliation loop which aims to 68 | // move the current state of the cluster closer to the desired state. 69 | // TODO(user): Modify the Reconcile function to compare the state specified by 70 | // the Server object against the actual cluster state, and then 71 | // perform operations to make the cluster state reflect the state specified by 72 | // the user. 73 | // 74 | // For more details, check Reconcile and its Result here: 75 | // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.2/pkg/reconcile 76 | func (r *ServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 77 | log := log.FromContext(ctx) 78 | 79 | instance := new(headscalev1alpha1.Server) 80 | if err := r.Get(ctx, req.NamespacedName, instance); err != nil { 81 | log.Error(err, "unable to fetch Server") 82 | // we'll ignore not-found errors, since they can't be fixed by an immediate 83 | // requeue (we'll need to wait for a new notification), and we can get them 84 | // on deleted requests. 85 | return ctrl.Result{}, client.IgnoreNotFound(err) 86 | } 87 | 88 | // examine DeletionTimestamp to determine if object is under deletion 89 | if instance.ObjectMeta.DeletionTimestamp.IsZero() { 90 | // The object is not being deleted, so if it does not have our finalizer, 91 | // then lets add the finalizer and update the object. This is equivalent 92 | // registering our finalizer. 93 | if !controllerutil.ContainsFinalizer(instance, Finalizer) { 94 | controllerutil.AddFinalizer(instance, Finalizer) 95 | if err := r.Update(ctx, instance); err != nil { 96 | return ctrl.Result{}, err 97 | } 98 | } 99 | } else { 100 | // The object is being deleted 101 | if controllerutil.ContainsFinalizer(instance, Finalizer) { 102 | 103 | // remove our finalizer from the list and update it. 104 | controllerutil.RemoveFinalizer(instance, Finalizer) 105 | if err := r.Update(ctx, instance); err != nil { 106 | return ctrl.Result{}, err 107 | } 108 | } 109 | 110 | // Stop reconciliation as the item is being deleted 111 | return ctrl.Result{}, nil 112 | } 113 | 114 | config := instance.Spec.Config 115 | 116 | log.Info("Config before default", "config", config) 117 | 118 | // Default value 119 | if err := mergo.Merge(&config, defaultServerConfig); err != nil { 120 | return ctrl.Result{}, err 121 | } 122 | 123 | log.Info("Config after default", "config", config) 124 | 125 | // _, grpcPort, err := utils.SliptHostPort(instance.Spec.Config.GRPCAddr) 126 | // if err != nil { 127 | // return ctrl.Result{}, err 128 | // } 129 | 130 | labels := labels.Set{ 131 | "app.kubernetes.io/name": "headscale", 132 | "app.kubernetes.io/instance": instance.Name, 133 | "app.kubernetes.io/managed-by": "headscale-operator", 134 | "app.kubernetes.io/component": "server", 135 | } 136 | 137 | //////////////// 138 | // Service 139 | //////////////// 140 | 141 | const grpcInsecurePort = 8082 142 | 143 | service := &corev1.Service{ 144 | ObjectMeta: metav1.ObjectMeta{ 145 | Name: fmt.Sprintf("%s-grpc", instance.Name), 146 | Namespace: instance.Namespace, 147 | }, 148 | } 149 | 150 | if op, err := controllerutil.CreateOrUpdate(ctx, r.Client, service, func() error { 151 | if err := controllerutil.SetControllerReference(instance, service, r.Scheme); err != nil { 152 | return err 153 | } 154 | 155 | service.ObjectMeta.Labels = labels 156 | 157 | service.Spec.Type = corev1.ServiceTypeClusterIP 158 | ports := []corev1.ServicePort{ 159 | { 160 | Name: "server", 161 | Protocol: corev1.ProtocolTCP, 162 | Port: 80, 163 | TargetPort: intstr.FromString("server"), 164 | }, 165 | { 166 | Name: "grpc-insecure", 167 | Protocol: corev1.ProtocolTCP, 168 | Port: int32(grpcInsecurePort), 169 | TargetPort: intstr.FromString("grpc-insecure"), 170 | }, 171 | } 172 | 173 | if err := mergo.Merge(&service.Spec.Ports, ports, mergo.WithOverride); err != nil { 174 | return err 175 | } 176 | 177 | service.Spec.Selector = labels 178 | 179 | return nil 180 | }); err != nil { 181 | r.recorder.Event(instance, "Warning", "Failed", fmt.Sprintf("Fail to reconcile Service %s", service.Name)) 182 | log.Error(err, "Service reconcile failed") 183 | } else { 184 | switch op { 185 | case controllerutil.OperationResultCreated: 186 | r.recorder.Event(instance, "Normal", "Created", fmt.Sprintf("Created Sevice %s", service.Name)) 187 | case controllerutil.OperationResultUpdated: 188 | r.recorder.Event(instance, "Normal", "Updated", fmt.Sprintf("Updated Sevice %s", service.Name)) 189 | } 190 | } 191 | 192 | if service.Spec.ClusterIP != "" { 193 | instance.Status.GrpcAddress = net.JoinHostPort(service.Spec.ClusterIP, strconv.Itoa(grpcInsecurePort)) 194 | if err := r.Status().Update(ctx, instance); err != nil { 195 | return ctrl.Result{}, err 196 | } 197 | } 198 | 199 | // //////////////// 200 | // // Service LB 201 | // //////////////// 202 | 203 | // serviceLB := &corev1.Service{ 204 | // ObjectMeta: metav1.ObjectMeta{ 205 | // Name: fmt.Sprintf("%s-lb", instance.Name), 206 | // Namespace: instance.Namespace, 207 | // }, 208 | // } 209 | 210 | // if op, err := controllerutil.CreateOrPatch(ctx, r.Client, serviceLB, func() error { 211 | // if err := controllerutil.SetControllerReference(instance, serviceLB, r.Scheme); err != nil { 212 | // return err 213 | // } 214 | 215 | // serviceLB.Spec.Type = corev1.ServiceTypeLoadBalancer 216 | // ports := []corev1.ServicePort{ 217 | // { 218 | // Name: "server", 219 | // Protocol: corev1.ProtocolTCP, 220 | // Port: 443, 221 | // TargetPort: intstr.FromString("server"), 222 | // }, 223 | // } 224 | 225 | // if err := mergo.Merge(&serviceLB.Spec.Ports, ports); err != nil { 226 | // return err 227 | // } 228 | 229 | // serviceLB.Spec.Selector = labels 230 | 231 | // return nil 232 | // }); err != nil { 233 | // log.Error(err, "Service LB reconcile failed") 234 | // } else { 235 | // switch op { 236 | // case controllerutil.OperationResultCreated: 237 | // r.recorder.Event(instance, "Normal", "Created", fmt.Sprintf("Created Sevice %s", service.Name)) 238 | // case controllerutil.OperationResultUpdated: 239 | // r.recorder.Event(instance, "Normal", "Updated", fmt.Sprintf("Updated Sevice %s", service.Name)) 240 | // } 241 | // } 242 | 243 | deployment := &appsv1.Deployment{ 244 | ObjectMeta: metav1.ObjectMeta{ 245 | Name: instance.Name, 246 | Namespace: instance.Namespace, 247 | }, 248 | } 249 | 250 | // certificate := &certmanagerv1.Certificate{ 251 | // ObjectMeta: metav1.ObjectMeta{ 252 | // Name: instance.Name, 253 | // Namespace: instance.Namespace, 254 | // }, 255 | // } 256 | 257 | // if op, err := controllerutil.CreateOrUpdate(ctx, r.Client, certificate, func() error { 258 | // if err := controllerutil.SetControllerReference(instance, certificate, r.Scheme); err != nil { 259 | // return err 260 | // } 261 | 262 | // certificate.Spec.SecretName = fmt.Sprintf("%s-tls", certificate.Name) 263 | // certificate.Spec.IssuerRef = certmanagerv1metav1.ObjectReference{ 264 | // Name: instance.Spec.Issuer, 265 | // } 266 | // certificate.Spec.DNSNames = []string{"test.com"} 267 | 268 | // ips := service.Spec.ClusterIPs 269 | // ips = append(ips, serviceLB.Spec.ClusterIPs...) 270 | // ips = append(ips, serviceLB.Spec.ExternalIPs...) 271 | // certificate.Spec.IPAddresses = ips 272 | 273 | // return nil 274 | // }); err != nil { 275 | // log.Error(err, "certificate reconcile failed") 276 | // } else { 277 | // if op != controllerutil.OperationResultNone { 278 | // log.Info("certificate successfully reconciled", "operation", op) 279 | // } 280 | // } 281 | 282 | // const keypath = "/run/headscale/certs" 283 | 284 | // instance.Spec.Config.TLSCertPath = path.Join(keypath, "tls.crt") 285 | // instance.Spec.Config.TLSKeyPath = path.Join(keypath, "tls.key") 286 | 287 | const runSocketsPath = "/run/headscale/socket" 288 | const socketName = "headscale.sock" 289 | 290 | config.UnixSocket = path.Join(runSocketsPath, socketName) 291 | config.UnixSocketPermission = "0770" 292 | 293 | const dataPath = "/var/lib/headscale" 294 | const sqlitename = "db.sqlite" 295 | 296 | config.DBtype = "sqlite3" 297 | config.DBpath = path.Join(dataPath, sqlitename) 298 | config.PrivateKeyPath = filepath.Join(dataPath, "private.key") 299 | 300 | config.Addr = "0.0.0.0:8080" 301 | config.ServerURL = fmt.Sprintf("https://%s", instance.Spec.Host) 302 | 303 | if instance.Spec.Debug { 304 | config.LogLevel = "debug" 305 | } 306 | 307 | // config.GRPCAllowInsecure = pointer.Bool(true) 308 | 309 | /////////////////////// 310 | // Configmap 311 | /////////////////////// 312 | 313 | configmap := &corev1.ConfigMap{ 314 | ObjectMeta: metav1.ObjectMeta{ 315 | Name: instance.Name, 316 | Namespace: instance.Namespace, 317 | }, 318 | Data: make(map[string]string), 319 | } 320 | 321 | if op, err := controllerutil.CreateOrUpdate(ctx, r.Client, configmap, func() error { 322 | if err := controllerutil.SetControllerReference(instance, configmap, r.Scheme); err != nil { 323 | return err 324 | } 325 | 326 | configmap.ObjectMeta.Labels = labels 327 | 328 | c, err := json.MarshalIndent(config, "", " ") 329 | if err != nil { 330 | return err 331 | } 332 | configmap.BinaryData = map[string][]byte{} 333 | 334 | configmap.Data[ConfigFileName] = string(c) 335 | 336 | return nil 337 | }); err != nil { 338 | r.recorder.Event(instance, "Warning", "Failed", fmt.Sprintf("Fail to reconcile Service %s", configmap.Name)) 339 | } else { 340 | switch op { 341 | case controllerutil.OperationResultCreated: 342 | r.recorder.Event(instance, "Normal", "Created", fmt.Sprintf("Created Configmap %s", configmap.Name)) 343 | case controllerutil.OperationResultUpdated: 344 | r.recorder.Event(instance, "Normal", "Updated", fmt.Sprintf("Updated Configmap %s", configmap.Name)) 345 | } 346 | } 347 | 348 | //////////////// 349 | // Deployment 350 | //////////////// 351 | 352 | if op, err := controllerutil.CreateOrUpdate(ctx, r.Client, deployment, func() error { 353 | if err := controllerutil.SetControllerReference(instance, deployment, r.Scheme); err != nil { 354 | return err 355 | } 356 | 357 | deployment.ObjectMeta.Labels = labels 358 | 359 | version := instance.Spec.Version 360 | if version == "" { 361 | version = "latest" 362 | } 363 | 364 | _, listenPort, err := utils.SliptHostPort(config.Addr) 365 | if err != nil { 366 | return fmt.Errorf("can't parse config.Addr %w", err) 367 | } 368 | 369 | _, metricsPort, err := utils.SliptHostPort(config.MetricsAddr) 370 | if err != nil { 371 | return fmt.Errorf("can't parse config.MetricsAddr %w", err) 372 | } 373 | 374 | // _, grpcPort, err := utils.SliptHostPort(instance.Spec.Config.GRPCAddr) 375 | // if err != nil { 376 | // return err 377 | // } 378 | 379 | // immuable 380 | if deployment.ObjectMeta.CreationTimestamp.IsZero() { 381 | deployment.Spec.Selector = metav1.SetAsLabelSelector(labels) 382 | } 383 | 384 | deployment.Spec.Replicas = pointer.Int32(1) 385 | podTemplate := corev1.PodTemplateSpec{ 386 | ObjectMeta: metav1.ObjectMeta{ 387 | Annotations: map[string]string{ 388 | "config-version": configmap.GetResourceVersion(), 389 | // "certificate-version": certificate.GetResourceVersion(), 390 | }, 391 | Labels: labels, 392 | }, 393 | Spec: corev1.PodSpec{ 394 | // InitContainers: []corev1.Container{ 395 | // { 396 | // Name: "init-litestream", 397 | // Image: "litestream/litestream:0.3.8", 398 | // Args: []string{"restore", "-if-db-not-exists", "-if-replica-exists", "-v", instance.Spec.Config.DBpath}, 399 | // VolumeMounts: []corev1.VolumeMount{ 400 | // { 401 | // Name: "data", 402 | // MountPath: sqlitedir, 403 | // }, 404 | // }, 405 | // }, 406 | // }, 407 | Containers: []corev1.Container{ 408 | { 409 | Name: "headscale", 410 | Image: fmt.Sprintf("headscale/headscale:%s", version), 411 | ImagePullPolicy: corev1.PullAlways, 412 | Command: []string{"headscale", "serve"}, 413 | VolumeMounts: []corev1.VolumeMount{ 414 | { 415 | Name: "config", 416 | ReadOnly: true, 417 | MountPath: "/etc/headscale/", 418 | }, 419 | // { 420 | // Name: "certificate", 421 | // ReadOnly: true, 422 | // MountPath: keypath, 423 | // }, 424 | { 425 | Name: "data", 426 | MountPath: dataPath, 427 | }, 428 | { 429 | Name: "run", 430 | MountPath: runSocketsPath, 431 | }, 432 | }, 433 | Ports: []corev1.ContainerPort{ 434 | { 435 | Name: "server", 436 | ContainerPort: int32(listenPort), 437 | }, 438 | { 439 | Name: "metrics", 440 | ContainerPort: int32(metricsPort), 441 | }, 442 | // { 443 | // Name: "grpc", 444 | // ContainerPort: int32(grpcPort), 445 | // }, 446 | }, 447 | LivenessProbe: &corev1.Probe{ 448 | ProbeHandler: corev1.ProbeHandler{ 449 | HTTPGet: &corev1.HTTPGetAction{ 450 | Port: intstr.FromString("server"), 451 | Path: "/health", 452 | Scheme: corev1.URISchemeHTTP, 453 | }, 454 | }, 455 | }, 456 | ReadinessProbe: &corev1.Probe{ 457 | ProbeHandler: corev1.ProbeHandler{ 458 | HTTPGet: &corev1.HTTPGetAction{ 459 | Port: intstr.FromString("server"), 460 | Path: "/health", 461 | Scheme: corev1.URISchemeHTTP, 462 | }, 463 | }, 464 | }, 465 | }, 466 | { 467 | Name: "socat", 468 | Image: "alpine/socat:1.7.4.3-r0", 469 | Args: []string{ 470 | "tcp-listen:8082,fork,reuseaddr", 471 | fmt.Sprintf("unix-connect:%s", path.Join(runSocketsPath, socketName)), 472 | }, 473 | Ports: []corev1.ContainerPort{ 474 | { 475 | Name: "grpc-insecure", 476 | ContainerPort: int32(grpcInsecurePort), 477 | }, 478 | }, 479 | VolumeMounts: []corev1.VolumeMount{ 480 | { 481 | Name: "run", 482 | ReadOnly: true, 483 | MountPath: runSocketsPath, 484 | }, 485 | }, 486 | LivenessProbe: &corev1.Probe{ 487 | ProbeHandler: corev1.ProbeHandler{ 488 | TCPSocket: &corev1.TCPSocketAction{ 489 | Port: intstr.FromString("grpc-insecure"), 490 | }, 491 | }, 492 | }, 493 | ReadinessProbe: &corev1.Probe{ 494 | ProbeHandler: corev1.ProbeHandler{ 495 | TCPSocket: &corev1.TCPSocketAction{ 496 | Port: intstr.FromString("grpc-insecure"), 497 | }, 498 | }, 499 | }, 500 | }, 501 | }, 502 | Volumes: []corev1.Volume{ 503 | { 504 | Name: "config", 505 | VolumeSource: corev1.VolumeSource{ 506 | ConfigMap: &corev1.ConfigMapVolumeSource{ 507 | LocalObjectReference: corev1.LocalObjectReference{ 508 | Name: configmap.Name, 509 | }, 510 | }, 511 | }, 512 | }, 513 | // { 514 | // Name: "certificate", 515 | // VolumeSource: corev1.VolumeSource{ 516 | // Secret: &corev1.SecretVolumeSource{ 517 | // SecretName: certificate.Spec.SecretName, 518 | // }, 519 | // }, 520 | // }, 521 | { 522 | Name: "data", 523 | VolumeSource: corev1.VolumeSource{ 524 | EmptyDir: &corev1.EmptyDirVolumeSource{}, 525 | }, 526 | }, 527 | { 528 | Name: "run", 529 | VolumeSource: corev1.VolumeSource{ 530 | EmptyDir: &corev1.EmptyDirVolumeSource{}, 531 | }, 532 | }, 533 | }, 534 | }, 535 | } 536 | 537 | if err := mergo.Merge(&deployment.Spec.Template, podTemplate, mergo.WithOverride); err != nil { 538 | return err 539 | } 540 | 541 | return nil 542 | }); err != nil { 543 | log.Error(err, "Deployment reconcile failed") 544 | } else { 545 | if op != controllerutil.OperationResultNone { 546 | log.Info("Deployment successfully reconciled", "operation", op) 547 | } 548 | instance.Status.DeploymentName = deployment.Name 549 | if err := r.Status().Update(ctx, instance); err != nil { 550 | return ctrl.Result{}, err 551 | } 552 | } 553 | 554 | // ingress := instance.Spec.Ingress 555 | // if ingress == nil { 556 | // ingress = &networkingv1.Ingress{} 557 | // } 558 | 559 | ingress := &networkingv1.Ingress{ 560 | ObjectMeta: metav1.ObjectMeta{ 561 | Name: instance.Name, 562 | Namespace: instance.Namespace, 563 | }, 564 | } 565 | 566 | // ingress.SetName(instance.Name) 567 | // ingress.SetNamespace(instance.Namespace) 568 | // ingress.ObjectMeta.Name = instance.Name 569 | // ingress.ObjectMeta.Namespace = instance.Namespace 570 | 571 | if op, err := controllerutil.CreateOrUpdate(ctx, r.Client, ingress, func() error { 572 | if err := controllerutil.SetControllerReference(instance, ingress, r.Scheme); err != nil { 573 | return err 574 | } 575 | 576 | if instance.Spec.Ingress != nil { 577 | if err := mergo.Merge(ingress, instance.Spec.Ingress); err != nil { 578 | return err 579 | } 580 | } 581 | 582 | instance.ObjectMeta.Labels = labels 583 | 584 | var prefixPathType = networkingv1.PathTypePrefix 585 | 586 | rules := []networkingv1.IngressRule{ 587 | { 588 | Host: instance.Spec.Host, 589 | IngressRuleValue: networkingv1.IngressRuleValue{ 590 | HTTP: &networkingv1.HTTPIngressRuleValue{ 591 | Paths: []networkingv1.HTTPIngressPath{ 592 | { 593 | Path: "/", 594 | // PathType: (*networkingv1.PathType)(pointer.String(string(networkingv1.PathTypePrefix))), 595 | PathType: &prefixPathType, 596 | Backend: networkingv1.IngressBackend{ 597 | Service: &networkingv1.IngressServiceBackend{ 598 | Name: service.GetName(), 599 | Port: networkingv1.ServiceBackendPort{ 600 | Name: "server", 601 | }, 602 | }, 603 | }, 604 | }, 605 | }, 606 | }, 607 | }, 608 | }, 609 | } 610 | 611 | if err := mergo.Merge(&ingress.Spec.Rules, rules, mergo.WithOverride); err != nil { 612 | return err 613 | } 614 | 615 | tls := []networkingv1.IngressTLS{ 616 | { 617 | Hosts: []string{ 618 | instance.Spec.Host, 619 | }, 620 | SecretName: fmt.Sprintf("%s-certificats", instance.GetName()), 621 | }, 622 | } 623 | 624 | if err := mergo.Merge(&ingress.Spec.TLS, tls, mergo.WithOverride); err != nil { 625 | return err 626 | } 627 | 628 | return nil 629 | }); err != nil { 630 | log.Error(err, "Fail to reconcile Ingress", "ingress", ingress) 631 | r.recorder.Event(instance, "Warning", "Failed", fmt.Sprintf("Fail to reconcile Ingress %s", ingress.Name)) 632 | } else { 633 | switch op { 634 | case controllerutil.OperationResultCreated: 635 | r.recorder.Event(instance, "Normal", "Created", fmt.Sprintf("Created Ingress %s", ingress.Name)) 636 | case controllerutil.OperationResultUpdated: 637 | r.recorder.Event(instance, "Normal", "Updated", fmt.Sprintf("Updated Ingress %s", ingress.Name)) 638 | } 639 | } 640 | 641 | return ctrl.Result{}, nil 642 | } 643 | 644 | // SetupWithManager sets up the controller with the Manager. 645 | func (r *ServerReconciler) SetupWithManager(mgr ctrl.Manager) error { 646 | r.recorder = mgr.GetEventRecorderFor("server-controller") 647 | 648 | return ctrl.NewControllerManagedBy(mgr). 649 | For(&headscalev1alpha1.Server{}). 650 | // Owns(&certmanagerv1.Certificate{}). 651 | Owns(&corev1.ConfigMap{}). 652 | Owns(&corev1.Service{}). 653 | Owns(&appsv1.Deployment{}). 654 | Owns(&networkingv1.Ingress{}). 655 | Complete(r) 656 | } 657 | -------------------------------------------------------------------------------- /controllers/server_controller_test.go: -------------------------------------------------------------------------------- 1 | package controllers_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | "github.com/tidwall/gjson" 11 | corev1 "k8s.io/api/core/v1" 12 | networkingv1 "k8s.io/api/networking/v1" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/utils/pointer" 15 | "sigs.k8s.io/controller-runtime/pkg/client" 16 | 17 | headscalev1alpha1 "github.com/guilhem/headscale-operator/api/v1alpha1" 18 | "github.com/guilhem/headscale-operator/controllers" 19 | "github.com/guilhem/headscale-operator/pkg/headscale" 20 | ) 21 | 22 | var _ = Describe("Controllers/ServerController", func() { 23 | const ( 24 | InstanceName = "test-server" 25 | InstanceNamespace = "default" 26 | Host = "test.domain.com" 27 | 28 | timeout = time.Second * 30 29 | duration = "10s" 30 | interval = "1s" 31 | ) 32 | 33 | Context("When creating new server", func() { 34 | It("Should create Server", func() { 35 | 36 | ctx := context.Background() 37 | 38 | By("Creating a new Instance") 39 | 40 | instance := &headscalev1alpha1.Server{ 41 | ObjectMeta: metav1.ObjectMeta{ 42 | Name: InstanceName, 43 | Namespace: InstanceNamespace, 44 | }, 45 | Spec: headscalev1alpha1.ServerSpec{ 46 | Version: "0.15.O", 47 | Ingress: &networkingv1.Ingress{ 48 | ObjectMeta: metav1.ObjectMeta{}, 49 | Spec: networkingv1.IngressSpec{ 50 | IngressClassName: pointer.String("ingress-class"), 51 | }, 52 | }, 53 | Config: headscale.Config{ 54 | LogLevel: "debug", 55 | }, 56 | Host: Host, 57 | }, 58 | } 59 | 60 | // ingress := &networkingv1.Ingress{ 61 | // ObjectMeta: metav1.ObjectMeta{}, 62 | // Spec: networkingv1.IngressSpec{ 63 | // IngressClassName: pointer.String("ingressClass"), 64 | // }, 65 | // } 66 | 67 | Expect(k8sClient.Create(ctx, instance)).Should(Succeed()) 68 | 69 | By("Should have deployment") 70 | 71 | Eventually(func() (int, error) { 72 | if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(instance), instance); err != nil { 73 | return -1, err 74 | 75 | } 76 | return len(instance.Status.DeploymentName), nil 77 | }, duration, interval).ShouldNot(Equal(0)) 78 | 79 | By("Should have the rigth configuration") 80 | 81 | Eventually(func() (string, error) { 82 | confimap := &corev1.ConfigMap{ 83 | ObjectMeta: metav1.ObjectMeta{ 84 | Name: InstanceName, 85 | Namespace: InstanceNamespace, 86 | }, 87 | } 88 | 89 | if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(confimap), confimap); err != nil { 90 | return "", err 91 | } 92 | 93 | json := confimap.Data[controllers.ConfigFileName] 94 | 95 | if !gjson.Valid(json) { 96 | return "", errors.New("not valid json") 97 | } 98 | 99 | logLevel := gjson.Get(json, "log_level") 100 | return logLevel.String(), nil 101 | 102 | }, duration, interval).Should(Equal("debug")) 103 | 104 | By("Should have ingress") 105 | 106 | ingress := &networkingv1.Ingress{ 107 | ObjectMeta: metav1.ObjectMeta{ 108 | Name: InstanceName, 109 | Namespace: InstanceNamespace, 110 | }, 111 | } 112 | 113 | Eventually(func() (int, error) { 114 | if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(ingress), ingress); err != nil { 115 | return -1, err 116 | } 117 | 118 | size := len(ingress.Spec.Rules) 119 | 120 | if ingress.Spec.Rules[0].Host != Host { 121 | return size, errors.New("host unmatch") 122 | } 123 | 124 | if *ingress.Spec.IngressClassName != "ingress-class" { 125 | return size, errors.New("missing ingress class") 126 | } 127 | return size, nil 128 | }, duration, interval).Should(Equal(1)) 129 | 130 | // By("Should have pod ready") 131 | 132 | // createdDeployment := &appsv1.Deployment{} 133 | 134 | // Eventually(func() (int, error) { 135 | // if err := k8sClient.Get(ctx, types.NamespacedName{Name: instance.Status.DeploymentName, Namespace: instance.Namespace}, createdDeployment); err != nil { 136 | // return -1, err 137 | // } 138 | 139 | // return int(createdDeployment.Status.AvailableReplicas), nil 140 | // }, duration, interval).ShouldNot(BeZero()) 141 | 142 | }) 143 | }) 144 | }) 145 | -------------------------------------------------------------------------------- /controllers/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Guilhem Lettron. 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 controllers_test 18 | 19 | import ( 20 | "context" 21 | "path/filepath" 22 | "testing" 23 | "time" 24 | 25 | ctrl "sigs.k8s.io/controller-runtime" 26 | 27 | certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" 28 | . "github.com/onsi/ginkgo/v2" 29 | . "github.com/onsi/gomega" 30 | "k8s.io/client-go/kubernetes/scheme" 31 | "k8s.io/client-go/rest" 32 | "sigs.k8s.io/controller-runtime/pkg/client" 33 | "sigs.k8s.io/controller-runtime/pkg/envtest" 34 | logf "sigs.k8s.io/controller-runtime/pkg/log" 35 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 36 | 37 | headscalev1alpha1 "github.com/guilhem/headscale-operator/api/v1alpha1" 38 | "github.com/guilhem/headscale-operator/controllers" 39 | //+kubebuilder:scaffold:imports 40 | ) 41 | 42 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 43 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 44 | 45 | var ( 46 | cfg *rest.Config 47 | k8sClient client.Client 48 | testEnv *envtest.Environment 49 | ctx context.Context 50 | cancel context.CancelFunc 51 | ) 52 | 53 | func TestAPIs(t *testing.T) { 54 | RegisterFailHandler(Fail) 55 | 56 | RunSpecs(t, "Controller Suite") 57 | } 58 | 59 | var _ = BeforeSuite(func() { 60 | logger := zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)) 61 | logf.SetLogger(logger) 62 | 63 | ctx, cancel = context.WithTimeout(context.TODO(), time.Minute) 64 | 65 | By("bootstrapping test environment") 66 | testEnv = &envtest.Environment{ 67 | CRDDirectoryPaths: []string{ 68 | filepath.Join("..", "config", "crd", "bases"), 69 | filepath.Join("testing-assets", "crd"), 70 | }, 71 | ErrorIfCRDPathMissing: true, 72 | AttachControlPlaneOutput: false, 73 | } 74 | 75 | var err error 76 | // cfg is defined in this file globally. 77 | cfg, err = testEnv.Start() 78 | Expect(err).NotTo(HaveOccurred()) 79 | Expect(cfg).NotTo(BeNil()) 80 | 81 | err = headscalev1alpha1.AddToScheme(scheme.Scheme) 82 | Expect(err).NotTo(HaveOccurred()) 83 | 84 | Expect(certmanagerv1.AddToScheme(scheme.Scheme)).NotTo(HaveOccurred()) 85 | 86 | //+kubebuilder:scaffold:scheme 87 | 88 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 89 | Expect(err).NotTo(HaveOccurred()) 90 | Expect(k8sClient).NotTo(BeNil()) 91 | 92 | ctrl.SetLogger(logger) 93 | 94 | k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ 95 | Scheme: scheme.Scheme, 96 | }) 97 | Expect(err).ToNot(HaveOccurred()) 98 | 99 | err = (&controllers.ServerReconciler{ 100 | Client: k8sManager.GetClient(), 101 | Scheme: k8sManager.GetScheme(), 102 | }).SetupWithManager(k8sManager) 103 | Expect(err).ToNot(HaveOccurred()) 104 | 105 | go func() { 106 | defer GinkgoRecover() 107 | err = k8sManager.Start(ctx) 108 | Expect(err).ToNot(HaveOccurred(), "failed to run manager") 109 | }() 110 | 111 | }) 112 | 113 | var _ = AfterSuite(func() { 114 | cancel() 115 | By("tearing down the test environment") 116 | err := testEnv.Stop() 117 | Expect(err).NotTo(HaveOccurred()) 118 | }) 119 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/guilhem/headscale-operator 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/cert-manager/cert-manager v1.8.0 7 | github.com/imdario/mergo v0.3.13 8 | github.com/juanfont/headscale v0.15.0 9 | github.com/onsi/ginkgo/v2 v2.1.4 10 | github.com/onsi/gomega v1.19.0 11 | golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf 12 | google.golang.org/grpc v1.44.0 13 | google.golang.org/protobuf v1.27.1 14 | k8s.io/api v0.24.0 15 | k8s.io/apimachinery v0.24.0 16 | k8s.io/client-go v0.24.0 17 | k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 18 | sigs.k8s.io/controller-runtime v0.12.1-0.20220602164547-f46919744bee 19 | ) 20 | 21 | require ( 22 | cloud.google.com/go v0.99.0 // indirect 23 | github.com/Azure/go-autorest v14.2.0+incompatible // indirect 24 | github.com/Azure/go-autorest/autorest v0.11.20 // indirect 25 | github.com/Azure/go-autorest/autorest/adal v0.9.15 // indirect 26 | github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect 27 | github.com/Azure/go-autorest/logger v0.2.1 // indirect 28 | github.com/Azure/go-autorest/tracing v0.6.0 // indirect 29 | github.com/PuerkitoBio/purell v1.1.1 // indirect 30 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 31 | github.com/beorn7/perks v1.0.1 // indirect 32 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 33 | github.com/davecgh/go-spew v1.1.1 // indirect 34 | github.com/emicklei/go-restful v2.9.5+incompatible // indirect 35 | github.com/evanphx/json-patch v4.12.0+incompatible // indirect 36 | github.com/fsnotify/fsnotify v1.5.1 // indirect 37 | github.com/go-logr/logr v1.2.0 // indirect 38 | github.com/go-logr/zapr v1.2.0 // indirect 39 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 40 | github.com/go-openapi/jsonreference v0.19.5 // indirect 41 | github.com/go-openapi/swag v0.19.14 // indirect 42 | github.com/gogo/protobuf v1.3.2 // indirect 43 | github.com/golang-jwt/jwt/v4 v4.0.0 // indirect 44 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 45 | github.com/golang/protobuf v1.5.2 // indirect 46 | github.com/google/gnostic v0.5.7-v3refs // indirect 47 | github.com/google/go-cmp v0.5.7 // indirect 48 | github.com/google/gofuzz v1.2.0 // indirect 49 | github.com/google/uuid v1.3.0 // indirect 50 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.3 // indirect 51 | github.com/josharian/intern v1.0.0 // indirect 52 | github.com/json-iterator/go v1.1.12 // indirect 53 | github.com/mailru/easyjson v0.7.6 // indirect 54 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect 55 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 56 | github.com/modern-go/reflect2 v1.0.2 // indirect 57 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 58 | github.com/pkg/errors v0.9.1 // indirect 59 | github.com/prometheus/client_golang v1.12.1 // indirect 60 | github.com/prometheus/client_model v0.2.0 // indirect 61 | github.com/prometheus/common v0.32.1 // indirect 62 | github.com/prometheus/procfs v0.7.3 // indirect 63 | github.com/spf13/pflag v1.0.5 // indirect 64 | github.com/tidwall/gjson v1.14.1 // indirect 65 | github.com/tidwall/match v1.1.1 // indirect 66 | github.com/tidwall/pretty v1.2.0 // indirect 67 | go.uber.org/atomic v1.7.0 // indirect 68 | go.uber.org/multierr v1.6.0 // indirect 69 | go.uber.org/zap v1.19.1 // indirect 70 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect 71 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect 72 | golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect 73 | golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 // indirect 74 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect 75 | golang.org/x/text v0.3.7 // indirect 76 | golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect 77 | gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect 78 | google.golang.org/appengine v1.6.7 // indirect 79 | google.golang.org/genproto v0.0.0-20220228195345-15d65a4533f7 // indirect 80 | gopkg.in/inf.v0 v0.9.1 // indirect 81 | gopkg.in/yaml.v2 v2.4.0 // indirect 82 | gopkg.in/yaml.v3 v3.0.0 // indirect 83 | k8s.io/apiextensions-apiserver v0.24.0 // indirect 84 | k8s.io/component-base v0.24.0 // indirect 85 | k8s.io/klog/v2 v2.60.1 // indirect 86 | k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 // indirect 87 | sigs.k8s.io/gateway-api v0.4.1 // indirect 88 | sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect 89 | sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect 90 | sigs.k8s.io/yaml v1.3.0 // indirect 91 | ) 92 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Guilhem Lettron. 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 | */ -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Guilhem Lettron. 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 | certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" 35 | headscalev1alpha1 "github.com/guilhem/headscale-operator/api/v1alpha1" 36 | "github.com/guilhem/headscale-operator/controllers" 37 | //+kubebuilder:scaffold:imports 38 | ) 39 | 40 | var ( 41 | scheme = runtime.NewScheme() 42 | setupLog = ctrl.Log.WithName("setup") 43 | ) 44 | 45 | func init() { 46 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 47 | 48 | utilruntime.Must(headscalev1alpha1.AddToScheme(scheme)) 49 | 50 | utilruntime.Must(certmanagerv1.AddToScheme(scheme)) 51 | //+kubebuilder:scaffold:scheme 52 | } 53 | 54 | func main() { 55 | var metricsAddr string 56 | var enableLeaderElection bool 57 | var probeAddr string 58 | flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") 59 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 60 | flag.BoolVar(&enableLeaderElection, "leader-elect", false, 61 | "Enable leader election for controller manager. "+ 62 | "Enabling this will ensure there is only one active controller manager.") 63 | opts := zap.Options{ 64 | Development: true, 65 | } 66 | opts.BindFlags(flag.CommandLine) 67 | flag.Parse() 68 | 69 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 70 | 71 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 72 | Scheme: scheme, 73 | MetricsBindAddress: metricsAddr, 74 | Port: 9443, 75 | HealthProbeBindAddress: probeAddr, 76 | LeaderElection: enableLeaderElection, 77 | LeaderElectionID: "67c702f3.barpilot.io", 78 | }) 79 | if err != nil { 80 | setupLog.Error(err, "unable to start manager") 81 | os.Exit(1) 82 | } 83 | 84 | if err := (&controllers.ServerReconciler{ 85 | Client: mgr.GetClient(), 86 | Scheme: mgr.GetScheme(), 87 | }).SetupWithManager(mgr); err != nil { 88 | setupLog.Error(err, "unable to create controller", "controller", "Server") 89 | os.Exit(1) 90 | } 91 | if err := (&controllers.NamespaceReconciler{ 92 | Client: mgr.GetClient(), 93 | Scheme: mgr.GetScheme(), 94 | }).SetupWithManager(mgr); err != nil { 95 | setupLog.Error(err, "unable to create controller", "controller", "Namespace") 96 | os.Exit(1) 97 | } 98 | if err := (&controllers.PreAuthKeyReconciler{ 99 | Client: mgr.GetClient(), 100 | Scheme: mgr.GetScheme(), 101 | }).SetupWithManager(mgr); err != nil { 102 | setupLog.Error(err, "unable to create controller", "controller", "PreAuthKey") 103 | os.Exit(1) 104 | } 105 | //+kubebuilder:scaffold:builder 106 | 107 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 108 | setupLog.Error(err, "unable to set up health check") 109 | os.Exit(1) 110 | } 111 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 112 | setupLog.Error(err, "unable to set up ready check") 113 | os.Exit(1) 114 | } 115 | 116 | setupLog.Info("starting manager") 117 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 118 | setupLog.Error(err, "problem running manager") 119 | os.Exit(1) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /pkg/headscale/config.go: -------------------------------------------------------------------------------- 1 | //+kubebuilder:object:generate=true 2 | package headscale 3 | 4 | import ( 5 | "crypto/tls" 6 | 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | ) 9 | 10 | type Config struct { 11 | ServerURL string `json:"server_url,omitempty"` 12 | Addr string `json:"listen_addr,omitempty"` 13 | MetricsAddr string `json:"metrics_listen_addr,omitempty"` 14 | GRPCAddr string `json:"grpc_listen_addr,omitempty"` 15 | GRPCAllowInsecure *bool `json:"grpc_allow_insecure,omitempty"` 16 | EphemeralNodeInactivityTimeout metav1.Duration `json:"ephemeral_node_inactivity_timeout,omitempty"` 17 | IPPrefixes []string `json:"ip_prefixes,omitempty"` 18 | PrivateKeyPath string `json:"private_key_path,omitempty"` 19 | 20 | DERP DERPConfig `json:"derp,omitempty"` 21 | 22 | DBtype string `json:"db_type,omitempty"` 23 | DBpath string `json:"db_path,omitempty"` 24 | DBhost string `json:"db_host,omitempty"` 25 | DBport int `json:"db_port,omitempty"` 26 | DBname string `json:"db_name,omitempty"` 27 | DBuser string `json:"db_user,omitempty"` 28 | DBpass string `json:"db_pass,omitempty"` 29 | 30 | TLSLetsEncryptListen string `json:"tls_letsencrypt_listen,omitempty"` 31 | TLSLetsEncryptHostname string `json:"tls_letsencrypt_hostname,omitempty"` 32 | TLSLetsEncryptCacheDir string `json:"tls_letsencrypt_cache_dir,omitempty"` 33 | TLSLetsEncryptChallengeType string `json:"tls_letsencrypt_challenge_type,omitempty"` 34 | 35 | TLSCertPath string `json:"tls_cert_path,omitempty"` 36 | TLSKeyPath string `json:"tls_key_path,omitempty"` 37 | TLSClientAuthMode tls.ClientAuthType `json:"tls_client_auth_mode,omitempty"` 38 | 39 | ACMEURL string `json:"acme_url,omitempty"` 40 | ACMEEmail string `json:"acme_email,omitempty"` 41 | 42 | DNSConfig DNSConfig `json:"dns_config,omitempty"` 43 | 44 | UnixSocket string `json:"unix_socket,omitempty"` 45 | UnixSocketPermission string `json:"unix_socket_permission,omitempty"` 46 | 47 | OIDC OIDCConfig `json:"oidc,omitempty"` 48 | 49 | LogTail LogTailConfig `json:"logtail,omitempty"` 50 | 51 | LogLevel string `json:"log_level,omitempty"` 52 | 53 | CLI CLIConfig `json:"cli,omitempty"` 54 | } 55 | 56 | type OIDCConfig struct { 57 | Issuer string `json:"issuer,omitempty"` 58 | ClientID string `json:"client_id,omitempty"` 59 | ClientSecret string `json:"client_secret,omitempty"` 60 | Scope []string `json:"scope,omitempty"` 61 | ExtraParams map[string]string `json:"extra_params,omitempty"` 62 | AllowedDomains []string `json:"allowed_domains,omitempty"` 63 | AllowedUsers []string `json:"allowed_users,omitempty"` 64 | StripEmaildomain *bool `json:"strip_email_domain,omitempty"` 65 | } 66 | 67 | type DERPConfig struct { 68 | Server DERPConfigServer `json:"server,omitempty"` 69 | AutoUpdate *bool `json:"auto_update_enabled,omitempty"` 70 | URLs []string `json:"urls,omitempty"` 71 | Paths []string `json:"paths,omitempty"` 72 | UpdateFrequency metav1.Duration `json:"update_frequency,omitempty"` 73 | } 74 | 75 | type DNSConfig struct { 76 | Magic *bool `json:"magic_dns,omitempty"` 77 | BaseDomain string `json:"base_domain,omitempty"` 78 | Nameservers []string `json:"nameservers,omitempty"` 79 | Domains []string `json:"domains,omitempty"` 80 | } 81 | 82 | type DERPConfigServer struct { 83 | Enabled *bool `json:"enabled,omitempty"` 84 | RegionCode string `json:"region_code,omitempty"` 85 | RegionName string `json:"region_name,omitempty"` 86 | STUNAddr string `json:"stun_listen_addr,omitempty"` 87 | RegionID int `json:"region_id,omitempty"` 88 | } 89 | 90 | type LogTailConfig struct { 91 | Enabled *bool `json:"enable,omitempty"` 92 | } 93 | 94 | type CLIConfig struct { 95 | Insecure *bool `json:"insecure,omitempty"` 96 | Address string `json:"address,omitempty"` 97 | APIKey string `json:"api_key,omitempty"` 98 | Timeout metav1.Duration `json:"timeout,omitempty"` 99 | } 100 | -------------------------------------------------------------------------------- /pkg/headscale/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | /* 5 | Copyright 2022 Guilhem Lettron. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | // Code generated by controller-gen. DO NOT EDIT. 21 | 22 | package headscale 23 | 24 | import () 25 | 26 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 27 | func (in *CLIConfig) DeepCopyInto(out *CLIConfig) { 28 | *out = *in 29 | if in.Insecure != nil { 30 | in, out := &in.Insecure, &out.Insecure 31 | *out = new(bool) 32 | **out = **in 33 | } 34 | out.Timeout = in.Timeout 35 | } 36 | 37 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CLIConfig. 38 | func (in *CLIConfig) DeepCopy() *CLIConfig { 39 | if in == nil { 40 | return nil 41 | } 42 | out := new(CLIConfig) 43 | in.DeepCopyInto(out) 44 | return out 45 | } 46 | 47 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 48 | func (in *Config) DeepCopyInto(out *Config) { 49 | *out = *in 50 | if in.GRPCAllowInsecure != nil { 51 | in, out := &in.GRPCAllowInsecure, &out.GRPCAllowInsecure 52 | *out = new(bool) 53 | **out = **in 54 | } 55 | out.EphemeralNodeInactivityTimeout = in.EphemeralNodeInactivityTimeout 56 | if in.IPPrefixes != nil { 57 | in, out := &in.IPPrefixes, &out.IPPrefixes 58 | *out = make([]string, len(*in)) 59 | copy(*out, *in) 60 | } 61 | in.DERP.DeepCopyInto(&out.DERP) 62 | in.DNSConfig.DeepCopyInto(&out.DNSConfig) 63 | in.OIDC.DeepCopyInto(&out.OIDC) 64 | in.LogTail.DeepCopyInto(&out.LogTail) 65 | in.CLI.DeepCopyInto(&out.CLI) 66 | } 67 | 68 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Config. 69 | func (in *Config) DeepCopy() *Config { 70 | if in == nil { 71 | return nil 72 | } 73 | out := new(Config) 74 | in.DeepCopyInto(out) 75 | return out 76 | } 77 | 78 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 79 | func (in *DERPConfig) DeepCopyInto(out *DERPConfig) { 80 | *out = *in 81 | in.Server.DeepCopyInto(&out.Server) 82 | if in.AutoUpdate != nil { 83 | in, out := &in.AutoUpdate, &out.AutoUpdate 84 | *out = new(bool) 85 | **out = **in 86 | } 87 | if in.URLs != nil { 88 | in, out := &in.URLs, &out.URLs 89 | *out = make([]string, len(*in)) 90 | copy(*out, *in) 91 | } 92 | if in.Paths != nil { 93 | in, out := &in.Paths, &out.Paths 94 | *out = make([]string, len(*in)) 95 | copy(*out, *in) 96 | } 97 | out.UpdateFrequency = in.UpdateFrequency 98 | } 99 | 100 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DERPConfig. 101 | func (in *DERPConfig) DeepCopy() *DERPConfig { 102 | if in == nil { 103 | return nil 104 | } 105 | out := new(DERPConfig) 106 | in.DeepCopyInto(out) 107 | return out 108 | } 109 | 110 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 111 | func (in *DERPConfigServer) DeepCopyInto(out *DERPConfigServer) { 112 | *out = *in 113 | if in.Enabled != nil { 114 | in, out := &in.Enabled, &out.Enabled 115 | *out = new(bool) 116 | **out = **in 117 | } 118 | } 119 | 120 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DERPConfigServer. 121 | func (in *DERPConfigServer) DeepCopy() *DERPConfigServer { 122 | if in == nil { 123 | return nil 124 | } 125 | out := new(DERPConfigServer) 126 | in.DeepCopyInto(out) 127 | return out 128 | } 129 | 130 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 131 | func (in *DNSConfig) DeepCopyInto(out *DNSConfig) { 132 | *out = *in 133 | if in.Magic != nil { 134 | in, out := &in.Magic, &out.Magic 135 | *out = new(bool) 136 | **out = **in 137 | } 138 | if in.Nameservers != nil { 139 | in, out := &in.Nameservers, &out.Nameservers 140 | *out = make([]string, len(*in)) 141 | copy(*out, *in) 142 | } 143 | if in.Domains != nil { 144 | in, out := &in.Domains, &out.Domains 145 | *out = make([]string, len(*in)) 146 | copy(*out, *in) 147 | } 148 | } 149 | 150 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSConfig. 151 | func (in *DNSConfig) DeepCopy() *DNSConfig { 152 | if in == nil { 153 | return nil 154 | } 155 | out := new(DNSConfig) 156 | in.DeepCopyInto(out) 157 | return out 158 | } 159 | 160 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 161 | func (in *LogTailConfig) DeepCopyInto(out *LogTailConfig) { 162 | *out = *in 163 | if in.Enabled != nil { 164 | in, out := &in.Enabled, &out.Enabled 165 | *out = new(bool) 166 | **out = **in 167 | } 168 | } 169 | 170 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LogTailConfig. 171 | func (in *LogTailConfig) DeepCopy() *LogTailConfig { 172 | if in == nil { 173 | return nil 174 | } 175 | out := new(LogTailConfig) 176 | in.DeepCopyInto(out) 177 | return out 178 | } 179 | 180 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 181 | func (in *OIDCConfig) DeepCopyInto(out *OIDCConfig) { 182 | *out = *in 183 | if in.Scope != nil { 184 | in, out := &in.Scope, &out.Scope 185 | *out = make([]string, len(*in)) 186 | copy(*out, *in) 187 | } 188 | if in.ExtraParams != nil { 189 | in, out := &in.ExtraParams, &out.ExtraParams 190 | *out = make(map[string]string, len(*in)) 191 | for key, val := range *in { 192 | (*out)[key] = val 193 | } 194 | } 195 | if in.AllowedDomains != nil { 196 | in, out := &in.AllowedDomains, &out.AllowedDomains 197 | *out = make([]string, len(*in)) 198 | copy(*out, *in) 199 | } 200 | if in.AllowedUsers != nil { 201 | in, out := &in.AllowedUsers, &out.AllowedUsers 202 | *out = make([]string, len(*in)) 203 | copy(*out, *in) 204 | } 205 | if in.StripEmaildomain != nil { 206 | in, out := &in.StripEmaildomain, &out.StripEmaildomain 207 | *out = new(bool) 208 | **out = **in 209 | } 210 | } 211 | 212 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCConfig. 213 | func (in *OIDCConfig) DeepCopy() *OIDCConfig { 214 | if in == nil { 215 | return nil 216 | } 217 | out := new(OIDCConfig) 218 | in.DeepCopyInto(out) 219 | return out 220 | } 221 | -------------------------------------------------------------------------------- /pkg/utils/grpc.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | 6 | headscaleapiv1 "github.com/juanfont/headscale/gen/go/headscale/v1" 7 | "google.golang.org/grpc" 8 | "google.golang.org/grpc/credentials/insecure" 9 | ) 10 | 11 | func NewHeadscaleServiceClient(ctx context.Context, address string) (headscaleapiv1.HeadscaleServiceClient, error) { 12 | grpcOptions := []grpc.DialOption{ 13 | grpc.WithBlock(), 14 | } 15 | 16 | grpcOptions = append(grpcOptions, 17 | grpc.WithTransportCredentials(insecure.NewCredentials()), 18 | ) 19 | 20 | // tlsConfig := &tls.Config{ 21 | // // turn of gosec as we are intentionally setting 22 | // // insecure. 23 | // //nolint:gosec 24 | // InsecureSkipVerify: true, 25 | // } 26 | 27 | // grpcOptions = append(grpcOptions, 28 | // grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)), 29 | // ) 30 | 31 | conn, err := grpc.DialContext(ctx, address, grpcOptions...) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | return headscaleapiv1.NewHeadscaleServiceClient(conn), nil 37 | } 38 | -------------------------------------------------------------------------------- /pkg/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "net" 5 | "strconv" 6 | "strings" 7 | 8 | "google.golang.org/grpc/codes" 9 | "google.golang.org/grpc/status" 10 | ) 11 | 12 | func SliptHostPort(hostport string) (string, int, error) { 13 | host, sListenPort, err := net.SplitHostPort(hostport) 14 | if err != nil { 15 | return "", 0, err 16 | } 17 | listenPort, err := strconv.Atoi(sListenPort) 18 | if err != nil { 19 | return "", 0, err 20 | } 21 | return host, listenPort, nil 22 | } 23 | 24 | func IgnoreNotFound(err error) error { 25 | status, ok := status.FromError(err) 26 | if !ok { 27 | return err 28 | } 29 | 30 | if (status.Code() == codes.NotFound) || strings.Contains(status.Message(), "not found") { 31 | return nil 32 | } 33 | 34 | return err 35 | } 36 | --------------------------------------------------------------------------------