From 4b003e21555854cc46671046db14ebcf5cf7a810 Mon Sep 17 00:00:00 2001 From: Axel Christ Date: Tue, 11 Apr 2023 16:40:33 +0200 Subject: [PATCH] Implement metalnetlet `metalnetlet` is a component that syncs required changes from `onmetal-api-net` to the `metalnet`-enabled cluster it is running in. Add tests, docker images, pipelines etc. --- .github/workflows/lint.yml | 4 + .github/workflows/publish-docker.yml | 7 + .github/workflows/test.yml | 4 + Dockerfile | 26 +- Makefile | 84 +++++-- api/v1alpha1/common_types.go | 11 + apiutils/apiutils.go | 29 +++ config/metalnetlet/default/kustomization.yaml | 142 +++++++++++ .../default/manager_auth_proxy_patch.yaml | 26 ++ .../default/manager_config_patch.yaml | 20 ++ config/metalnetlet/kind/add-manager-args.yaml | 3 + config/metalnetlet/kind/kustomization.yaml | 18 ++ .../kind/metalnet-rbac/kustomization.yaml | 17 ++ config/metalnetlet/kind/patch-controller.yaml | 11 + .../manager/controller_manager_config.yaml | 11 + config/metalnetlet/manager/kustomization.yaml | 10 + config/metalnetlet/manager/manager.yaml | 58 +++++ .../metalnet-rbac/kustomization.yaml | 7 + config/metalnetlet/metalnet-rbac/role.yaml | 26 ++ .../metalnet-rbac/role_binding.yaml | 13 + .../metalnet-rbac/service_account.yaml | 5 + .../metalnetlet/prometheus/kustomization.yaml | 2 + config/metalnetlet/prometheus/monitor.yaml | 20 ++ .../rbac/auth_proxy_client_clusterrole.yaml | 9 + config/metalnetlet/rbac/auth_proxy_role.yaml | 17 ++ .../rbac/auth_proxy_role_binding.yaml | 12 + .../metalnetlet/rbac/auth_proxy_service.yaml | 14 ++ config/metalnetlet/rbac/kustomization.yaml | 18 ++ .../rbac/leader_election_role.yaml | 37 +++ .../rbac/leader_election_role_binding.yaml | 12 + config/metalnetlet/rbac/role.yaml | 53 +++++ config/metalnetlet/rbac/role_binding.yaml | 12 + config/metalnetlet/rbac/service_account.yaml | 5 + .../onmetal-api-net/rbac/kustomization.yaml | 5 + .../rbac/metalnetlet_bootstrapper_role.yaml | 20 ++ .../metalnetlet_bootstrapper_rolebinding.yaml | 14 ++ .../rbac/metalnetlet_role.yaml | 19 ++ .../rbac/metalnetlet_rolebinding.yaml | 12 + go.mod | 1 + go.sum | 2 + hack/setup-git-redirect.sh | 16 ++ metalnetlet/client/config/getter.go | 54 +++++ .../controllers/controllers_suite_test.go | 139 +++++++++++ metalnetlet/controllers/network_controller.go | 225 ++++++++++++++++++ .../controllers/network_controller_test.go | 66 +++++ metalnetlet/controllers/rbac.go | 26 ++ .../testdata/metalnet-crds/network.yaml | 67 ++++++ metalnetlet/main.go | 164 +++++++++++++ .../onmetal-api-net/metalnetlet.go | 90 +++++++ 49 files changed, 1647 insertions(+), 16 deletions(-) create mode 100644 apiutils/apiutils.go create mode 100644 config/metalnetlet/default/kustomization.yaml create mode 100644 config/metalnetlet/default/manager_auth_proxy_patch.yaml create mode 100644 config/metalnetlet/default/manager_config_patch.yaml create mode 100644 config/metalnetlet/kind/add-manager-args.yaml create mode 100644 config/metalnetlet/kind/kustomization.yaml create mode 100644 config/metalnetlet/kind/metalnet-rbac/kustomization.yaml create mode 100644 config/metalnetlet/kind/patch-controller.yaml create mode 100644 config/metalnetlet/manager/controller_manager_config.yaml create mode 100644 config/metalnetlet/manager/kustomization.yaml create mode 100644 config/metalnetlet/manager/manager.yaml create mode 100644 config/metalnetlet/metalnet-rbac/kustomization.yaml create mode 100644 config/metalnetlet/metalnet-rbac/role.yaml create mode 100644 config/metalnetlet/metalnet-rbac/role_binding.yaml create mode 100644 config/metalnetlet/metalnet-rbac/service_account.yaml create mode 100644 config/metalnetlet/prometheus/kustomization.yaml create mode 100644 config/metalnetlet/prometheus/monitor.yaml create mode 100644 config/metalnetlet/rbac/auth_proxy_client_clusterrole.yaml create mode 100644 config/metalnetlet/rbac/auth_proxy_role.yaml create mode 100644 config/metalnetlet/rbac/auth_proxy_role_binding.yaml create mode 100644 config/metalnetlet/rbac/auth_proxy_service.yaml create mode 100644 config/metalnetlet/rbac/kustomization.yaml create mode 100644 config/metalnetlet/rbac/leader_election_role.yaml create mode 100644 config/metalnetlet/rbac/leader_election_role_binding.yaml create mode 100644 config/metalnetlet/rbac/role.yaml create mode 100644 config/metalnetlet/rbac/role_binding.yaml create mode 100644 config/metalnetlet/rbac/service_account.yaml create mode 100644 config/onmetal-api-net/rbac/metalnetlet_bootstrapper_role.yaml create mode 100644 config/onmetal-api-net/rbac/metalnetlet_bootstrapper_rolebinding.yaml create mode 100644 config/onmetal-api-net/rbac/metalnetlet_role.yaml create mode 100644 config/onmetal-api-net/rbac/metalnetlet_rolebinding.yaml create mode 100755 hack/setup-git-redirect.sh create mode 100644 metalnetlet/client/config/getter.go create mode 100644 metalnetlet/controllers/controllers_suite_test.go create mode 100644 metalnetlet/controllers/network_controller.go create mode 100644 metalnetlet/controllers/network_controller_test.go create mode 100644 metalnetlet/controllers/rbac.go create mode 100644 metalnetlet/controllers/testdata/metalnet-crds/network.yaml create mode 100644 metalnetlet/main.go create mode 100644 onmetal-api-net/controllers/certificate/onmetal-api-net/metalnetlet.go diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f5f91da5..ffbf17e7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,6 +14,10 @@ jobs: - uses: actions/setup-go@v4 with: go-version-file: 'go.mod' + - uses: webfactory/ssh-agent@v0.8.0 + with: + ssh-private-key: ${{ secrets.BOT_PRIVATE_KEY }} + - run: ./hack/setup-git-redirect.sh - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index f5cddff8..61d6d67a 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -28,10 +28,15 @@ jobs: target: onmetal-api-net-manager - name: apinetlet target: apinetlet-manager + - name: metalnetlet + target: metalnetlet-manager if: ${{ github.event.label.name == 'ok-to-image' }} || ${{ github.event.label.name == 'ok-to-🐳' }} || ${{ github.ref == 'refs/heads/main' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - uses: webfactory/ssh-agent@v0.8.0 + with: + ssh-private-key: ${{ secrets.BOT_PRIVATE_KEY }} - uses: docker/metadata-action@v4 id: meta with: @@ -77,3 +82,5 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} target: ${{ matrix.image.target }} + ssh: | + default=${{ env.SSH_AUTH_SOCK }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d6e8a60c..0d67b626 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,4 +16,8 @@ jobs: - uses: actions/setup-go@v4 with: go-version-file: 'go.mod' + - uses: webfactory/ssh-agent@v0.8.0 + with: + ssh-private-key: ${{ secrets.BOT_PRIVATE_KEY }} + - run: ./hack/setup-git-redirect.sh - run: make test diff --git a/Dockerfile b/Dockerfile index f1c646e7..12624cb4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,15 +6,27 @@ WORKDIR /workspace COPY go.mod go.mod COPY go.sum go.sum +COPY hack hack + +ENV GOPRIVATE='github.com/onmetal/*' + # cache deps before building and copying source so that we don't need to re-download as much # and so that source changes don't invalidate our downloaded layer -RUN go mod download +RUN --mount=type=ssh --mount=type=secret,id=github_pat \ + --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg \ + GITHUB_PAT_PATH=/run/secrets/github_pat ./hack/setup-git-redirect.sh \ + && mkdir -p -m 0600 ~/.ssh \ + && ssh-keyscan github.com >> ~/.ssh/known_hosts \ + && go mod download # Copy the go source COPY api/ api/ +COPY apiutils/ apiutils/ COPY apinetlet/ apinetlet/ COPY flag/ flag/ COPY onmetal-api-net/ onmetal-api-net/ +COPY metalnetlet/ metalnetlet/ ARG TARGETOS ARG TARGETARCH @@ -25,7 +37,8 @@ RUN mkdir bin RUN --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg \ CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH GO111MODULE=on go build -ldflags="-s -w" -a -o bin/onmetal-api-net-manager ./onmetal-api-net && \ - CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH GO111MODULE=on go build -ldflags="-s -w" -a -o bin/apinetlet-manager ./apinetlet + CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH GO111MODULE=on go build -ldflags="-s -w" -a -o bin/apinetlet-manager ./apinetlet && \ + CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH GO111MODULE=on go build -ldflags="-s -w" -a -o bin/metalnetlet-manager ./metalnetlet # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details @@ -44,3 +57,12 @@ COPY --from=builder /workspace/bin/apinetlet-manager /manager USER 65532:65532 ENTRYPOINT ["/manager"] + +# Use distroless as minimal base image to package the manager binary +# Refer to https://github.com/GoogleContainerTools/distroless for more details +FROM gcr.io/distroless/static:nonroot AS metalnetlet-manager +WORKDIR / +COPY --from=builder /workspace/bin/metalnetlet-manager /manager +USER 65532:65532 + +ENTRYPOINT ["/manager"] diff --git a/Makefile b/Makefile index d97e8d10..82da364a 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ # Image URL to use all building/pushing image targets ONMETAL_API_NET_IMG ?= onmetal-api-net:latest APINETLET_IMG ?= apinetlet:latest +METALNETLET_IMG ?= metalnetlet:latest KIND_CLUSTER_NAME ?= kind # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. @@ -14,6 +15,8 @@ else GOBIN=$(shell go env GOBIN) endif +SSH_KEY ?= ${HOME}/.ssh/id_rsa + # Setting SHELL to bash allows bash commands to be executed by recipes. # This is a requirement for 'setup-envtest.sh' in the test target. # Options are set to exit when a recipe line exits non-zero or a piped command fails. @@ -48,6 +51,7 @@ manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and Cust # apinetlet $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role paths="./apinetlet/..." output:rbac:artifacts:config=config/apinetlet/rbac + $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role paths="./metalnetlet/..." output:rbac:artifacts:config=config/metalnetlet/rbac # poollet system roles cp config/apinetlet/apinet-rbac/role.yaml config/onmetal-api-net/rbac/apinetlet_role.yaml @@ -93,8 +97,12 @@ build-onmetal-api-net: generate fmt addlicense lint ## Build onmetal-api-net bin build-apinetlet: generate fmt addlicense lint ## Build apinetlet. go build -o bin/apinetlet ./apinetlet/main.go +.PHONY: build-metalnetlet +build-metalnetlet: generate fmt addlicense lint ## Build metalnetlet. + go build -o bin/metalnetlet ./metalnetlet/main.go + .PHONY: build -build: build-onmetal-api-net build-apinetlet ## Build onmetal-api-net and apinetlet. +build: build-onmetal-api-net build-apinetlet build-metalnetlet ## Build onmetal-api-net, apinetlet, metalnetlet. .PHONY: run-onmetal-api-net run-onmetal-api-net: manifests generate fmt lint ## Run a onmetal-api-net from your host. @@ -104,16 +112,24 @@ run-onmetal-api-net: manifests generate fmt lint ## Run a onmetal-api-net from y run-apinetlet: manifests generate fmt lint ## Run apinetlet from your host. go run ./apinetlet/main.go +.PHONY: run-metalnetlet +run-metalnetlet: manifests generate fmt lint ## Run metalnetlet from your host. + go run ./metalnetlet/main.go + .PHONY: docker-build-onmetal-api-net docker-build-onmetal-api-net: ## Build onmetal-api-net image with the manager. - docker build --target onmetal-api-net-manager -t ${ONMETAL_API_NET_IMG} . + docker build --ssh default=${SSH_KEY} --target onmetal-api-net-manager -t ${ONMETAL_API_NET_IMG} . .PHONY: docker-build-apinetlet docker-build-apinetlet: ## Build apinetlet image with the manager. - docker build --target apinetlet-manager -t ${APINETLET_IMG} . + docker build --ssh default=${SSH_KEY} --target apinetlet-manager -t ${APINETLET_IMG} . + +.PHONY: docker-build-metalnetlet +docker-build-metalnetlet: ## Build metalnetlet image with the manager. + docker build --ssh default=${SSH_KEY} --target metalnetlet-manager -t ${METALNETLET_IMG} . .PHONY: docker-build -docker-build: docker-build-onmetal-api-net docker-build-apinetlet +docker-build: docker-build-onmetal-api-net docker-build-apinetlet docker-build-metalnetlet ## Build docker images. .PHONY: docker-push-onmetal-api-net docker-push-onmetal-api-net: ## Push onmetal-api-net image. @@ -123,8 +139,12 @@ docker-push-onmetal-api-net: ## Push onmetal-api-net image. docker-push-apinetlet: ## Push apinetlet image. docker push ${APINETLET_IMG} +.PHONY: docker-push-metalnetlet +docker-push-metalnetlet: ## Push metalnetlet image. + docker push ${METALNETLET_IMG} + .PHONY: docker-push -docker-push: docker-push-onmetal-api-net docker-build-apinetlet ## Push onmetal-api-net and apinetlet image. +docker-push: docker-push-onmetal-api-net docker-build-apinetlet docker-build-metalnetlet ## Push onmetal-api-net, apinetlet, metalnetlet image. ##@ Deployment @@ -144,11 +164,19 @@ install-apinetlet: manifests ## Install apinetlet CRDs into the K8s cluster spec uninstall-apinetlet: manifests ## Uninstall apinetlet CRDs from the K8s cluster specified in ~/.kube/config. kubectl delete-k config/apinetlet/crd +.PHONY: install-metalnetlet +install-metalnetlet: manifests ## Install metalnetlet CRDs into the K8s cluster specified in ~/.kube/config. + kubectl apply -k config/metalnetlet/crd + +.PHONY: uninstall-metalnetlet +uninstall-metalnetlet: manifests ## Uninstall metalnetlet CRDs from the K8s cluster specified in ~/.kube/config. + kubectl delete-k config/metalnetlet/crd + .PHONY: install -install: install-onmetal-api-net install-apinetlet ## Uninstall onmetal-api-net and apinetlet. +install: install-onmetal-api-net install-apinetlet install-metalnetlet ## Uninstall onmetal-api-net, apinetlet, metalnetlet. .PHONY: uninstall -uninstall: uninstall-onmetal-api-net uninstall-apinetlet ## Uninstall onmetal-api-net and apinetlet. +uninstall: uninstall-onmetal-api-net uninstall-apinetlet uninstall-metalnetlet ## Uninstall onmetal-api-net, apinetlet, metalnetlet. .PHONY: deploy-onmetal-api-net deploy-onmetal-api-net: manifests kustomize ## Deploy onmetal-api-net controller to the K8s cluster specified in ~/.kube/config. @@ -160,8 +188,13 @@ deploy-apinetlet: manifests kustomize ## Deploy apinetlet controller to the K8s cd config/apinetlet/manager && $(KUSTOMIZE) edit set image controller=${APINETLET_IMG} kubectl apply -k config/apinetlet/default +.PHONY: deploy-metalnetlet +deploy-metalnetlet: manifests kustomize ## Deploy metalnetlet controller to the K8s cluster specified in ~/.kube/config. + cd config/metalnetlet/manager && $(KUSTOMIZE) edit set image controller=${METALNETLET_IMG} + kubectl apply -k config/metalnetlet/default + .PHONY: deploy -deploy: deploy-onmetal-api-net deploy-apinetlet +deploy: deploy-onmetal-api-net deploy-apinetlet deploy-metalnetlet ## Deploy onmetal-api-net, apinetlet, metalnetlet .PHONY: undeploy-onmetal-api-net undeploy-onmetal-api-net: ## Undeploy onmetal-api-net controller from the K8s cluster specified in ~/.kube/config. @@ -171,8 +204,12 @@ undeploy-onmetal-api-net: ## Undeploy onmetal-api-net controller from the K8s cl undeploy-apinetlet: ## Undeploy apinetlet controller from the K8s cluster specified in ~/.kube/config. kubectl delete -k config/apinetlet +.PHONY: undeploy-metalnetlet +undeploy-metalnetlet: ## Undeploy metalnetlet controller from the K8s cluster specified in ~/.kube/config. + kubectl delete -k config/metalnetlet + .PHONY: undeploy -undeploy: undeploy-onmetal-api-net undeploy-apinetlet ## Undeploy onmetal-api-net and apinetlet controller from the K8s cluster specified in ~/.kube/config. +undeploy: undeploy-onmetal-api-net undeploy-apinetlet undeploy-metalnetlet ## Undeploy onmetal-api-net, apinetlet, metalnetlet controller from the K8s cluster specified in ~/.kube/config. ##@ Kind Deployment plumbing @@ -184,8 +221,12 @@ kind-load-onmetal-api-net: docker-build-onmetal-api-net ## Load onmetal-api-net kind-load-apinetlet: docker-build-apinetlet ## Load apinetlet image to kind cluster. kind load docker-image --name ${KIND_CLUSTER_NAME} ${APINETLET_IMG} +.PHONY: kind-load-metalnetlet +kind-load-metalnetlet: docker-build-metalnetlet ## Load metalnetlet image to kind cluster. + kind load docker-image --name ${KIND_CLUSTER_NAME} ${METALNETLET_IMG} + .PHONY: kind-load -kind-load: kind-load-onmetal-api-net kind-load-apinetlet ## Load onmetal-api-net and apinetlet image to kind cluster. +kind-load: kind-load-onmetal-api-net kind-load-apinetlet kind-load-metalnetlet ## Load onmetal-api-net, apinetlet, metalnetlet image to kind cluster. .PHONY: kind-restart-onmetal-api-net kind-restart-onmetal-api-net: ## Restarts the onmetal-api-net controller manager. @@ -195,8 +236,12 @@ kind-restart-onmetal-api-net: ## Restarts the onmetal-api-net controller manager kind-restart-apinetlet: ## Restarts the apinetlet controller manager. kubectl -n apinetlet-system delete rs -l control-plane=controller-manager +.PHONY: kind-restart-metalnetlet +kind-restart-metalnetlet: ## Restarts the metalnetlet controller manager. + kubectl -n metalnetlet-system delete rs -l control-plane=controller-manager + .PHONY: kind-restart -kind-restart: kind-restart-onmetal-api-net kind-restart-apinetlet ## Restarts the onmetal-api-net and apinetlet controller manager. +kind-restart: kind-restart-onmetal-api-net kind-restart-apinetlet kind-restart-metalnetlet ## Restarts the onmetal-api-net, apinetlet, metalnetlet controller manager. .PHONY: kind-build-load-restart-onmetal-api-net kind-build-load-restart-onmetal-api-net: docker-build-onmetal-api-net kind-load-onmetal-api-net kind-restart-onmetal-api-net ## Build, load and restart onmetal-api-net. @@ -204,8 +249,11 @@ kind-build-load-restart-onmetal-api-net: docker-build-onmetal-api-net kind-load- .PHONY: kind-build-load-restart-apinetlet kind-build-load-restart-apinetlet: docker-build-apinetlet kind-load-apinetlet kind-restart-apinetlet ## Build, load and restart apinetlet. +.PHONY: kind-build-load-restart-metalnetlet +kind-build-load-restart-metalnetlet: docker-build-metalnetlet kind-load-metalnetlet kind-restart-metalnetlet ## Build, load and restart metalnetlet. + .PHONY: kind-build-load-restart -kind-build-load-restart: kind-build-load-restart-onmetal-api-net kind-build-load-restart-apinetlet +kind-build-load-restart: kind-build-load-restart-onmetal-api-net kind-build-load-restart-apinetlet kind-build-load-restart-metalnetlet .PHONY: kind-apply-onmetal-api-net kind-apply-onmetal-api-net: manifests ## Apply onmetal-api-net to the cluster specified in ~/.kube/config. @@ -215,8 +263,12 @@ kind-apply-onmetal-api-net: manifests ## Apply onmetal-api-net to the cluster sp kind-apply-apinetlet: manifests ## Apply apinetlet to the cluster specified in ~/.kube/config. kubectl apply -k config/apinetlet/kind +.PHONY: kind-apply-metalnetlet +kind-apply-metalnetlet: manifests ## Apply metalnetlet to the cluster specified in ~/.kube/config. + kubectl apply -k config/metalnetlet/kind + .PHONY: kind-apply -kind-apply: kind-apply-onmetal-api-net kind-apply-apinetlet ## Apply onmetal-api-net apinetlet to the cluster specified in ~/.kube/config. +kind-apply: kind-apply-onmetal-api-net kind-apply-apinetlet kind-apply-metalnetlet ## Apply onmetal-api-net, apinetlet, metalnetlet to the cluster specified in ~/.kube/config. .PHONY: kind-deploy kind-deploy: kind-build-load-restart kind-apply ## Build, load and restart onmetal-api-net and apinetlet and apply them. @@ -229,8 +281,12 @@ kind-delete-onmetal-api-net: ## Delete onmetal-api-net from the cluster specifie kind-delete-apinetlet: ## Delete apinetlet from the cluster specified in ~/.kube/config. kubectl delete -k config/apinetlet/kind +.PHONY: kind-delete-metalnetlet +kind-delete-metalnetlet: ## Delete metalnetlet from the cluster specified in ~/.kube/config. + kubectl delete -k config/metalnetlet/kind + .PHONY: kind-delete -kind-delete: kind-delete-onmetal-api-net kind-delete-apinetlet ## Delete onmetal-api-net and apinetlet from the cluster specified in ~/.kube/config. +kind-delete: kind-delete-onmetal-api-net kind-delete-apinetlet kind-delete-metalnetlet ## Delete onmetal-api-net, apinetlet, metalnetlet from the cluster specified in ~/.kube/config. ##@ Tools diff --git a/api/v1alpha1/common_types.go b/api/v1alpha1/common_types.go index 6b78541d..7f5e3c2f 100644 --- a/api/v1alpha1/common_types.go +++ b/api/v1alpha1/common_types.go @@ -30,6 +30,12 @@ const ( // APINetletUserNamePrefix is the prefix all apinetlet users should have. APINetletUserNamePrefix = "apinet.api.onmetal.de:system:apinetlet:" + + // MetalnetletsGroup is the system rbac group all metalnetlets are in. + MetalnetletsGroup = "apinet.api.onmetal.de:system:metalnetlets" + + // MetalnetletUserNamePrefix is the prefix all metalnetlet users should have. + MetalnetletUserNamePrefix = "apinet.api.onmetal.de:system:metalnetlet:" ) // APINetletCommonName constructs the common name for a certificate of an apinetlet user. @@ -37,6 +43,11 @@ func APINetletCommonName(name string) string { return APINetletUserNamePrefix + name } +// MetalnetletCommonName constructs the common name for a certificate of a metalnetlet user. +func MetalnetletCommonName(name string) string { + return MetalnetletUserNamePrefix + name +} + // IP is an IP address. // +kubebuilder:validation:Type=string type IP struct { diff --git a/apiutils/apiutils.go b/apiutils/apiutils.go new file mode 100644 index 00000000..32205725 --- /dev/null +++ b/apiutils/apiutils.go @@ -0,0 +1,29 @@ +// Copyright 2023 OnMetal authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apiutils + +import ( + "github.com/onmetal/onmetal-api-net/api/v1alpha1" + corev1 "k8s.io/api/core/v1" +) + +func IsNetworkAllocated(network *v1alpha1.Network) bool { + for _, condition := range network.Status.Conditions { + if condition.Type == v1alpha1.NetworkAllocated { + return condition.Status == corev1.ConditionTrue + } + } + return false +} diff --git a/config/metalnetlet/default/kustomization.yaml b/config/metalnetlet/default/kustomization.yaml new file mode 100644 index 00000000..d7d56b50 --- /dev/null +++ b/config/metalnetlet/default/kustomization.yaml @@ -0,0 +1,142 @@ +# Adds namespace to all resources. +namespace: metalnetlet-system + +# Value of this field is prepended to the +# names of all resources, e.g. a deployment named +# "wordpress" becomes "alices-wordpress". +# Note that it should also match with the prefix (text before '-') of the namespace +# field above. +namePrefix: metalnetlet- + +# Labels to add to all resources and selectors. +#commonLabels: +# someName: someValue + +resources: +- ../rbac +- ../manager +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- ../webhook +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. +#- ../certmanager +# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. +#- ../prometheus + +patchesStrategicMerge: +# Protect the /metrics endpoint by putting it behind auth. +# If you want your controller-manager to expose the /metrics +# endpoint w/o any authn/z, please comment the following line. +- manager_auth_proxy_patch.yaml + +# Mount the controller config file for loading manager configurations +# through a ComponentConfig type +#- manager_config_patch.yaml + +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- manager_webhook_patch.yaml + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. +# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. +# 'CERTMANAGER' needs to be enabled to use ca injection +#- webhookcainjection_patch.yaml + +# replacement once webhooks are enabled +#replacements: +# - source: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# fieldPath: .metadata.namespace +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - select: +# kind: CustomResourceDefinition +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - source: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# fieldPath: .metadata.name +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - select: +# kind: CustomResourceDefinition +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - source: +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.name +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 0 +# create: true +# - source: +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.namespace +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 1 +# create: true diff --git a/config/metalnetlet/default/manager_auth_proxy_patch.yaml b/config/metalnetlet/default/manager_auth_proxy_patch.yaml new file mode 100644 index 00000000..a224be19 --- /dev/null +++ b/config/metalnetlet/default/manager_auth_proxy_patch.yaml @@ -0,0 +1,26 @@ +# This patch inject a sidecar container which is a HTTP proxy for the +# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: kube-rbac-proxy + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=10" + ports: + - containerPort: 8443 + name: https + - name: manager + args: + - "--health-probe-bind-address=:8081" + - "--metrics-bind-address=127.0.0.1:8080" + - "--leader-elect" diff --git a/config/metalnetlet/default/manager_config_patch.yaml b/config/metalnetlet/default/manager_config_patch.yaml new file mode 100644 index 00000000..6c400155 --- /dev/null +++ b/config/metalnetlet/default/manager_config_patch.yaml @@ -0,0 +1,20 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager + args: + - "--config=controller_manager_config.yaml" + volumeMounts: + - name: manager-config + mountPath: /controller_manager_config.yaml + subPath: controller_manager_config.yaml + volumes: + - name: manager-config + configMap: + name: manager-config diff --git a/config/metalnetlet/kind/add-manager-args.yaml b/config/metalnetlet/kind/add-manager-args.yaml new file mode 100644 index 00000000..e15e1928 --- /dev/null +++ b/config/metalnetlet/kind/add-manager-args.yaml @@ -0,0 +1,3 @@ +- op: add + path: /spec/template/spec/containers/0/args/- + value: --name=metalnetlet diff --git a/config/metalnetlet/kind/kustomization.yaml b/config/metalnetlet/kind/kustomization.yaml new file mode 100644 index 00000000..1c5ec49e --- /dev/null +++ b/config/metalnetlet/kind/kustomization.yaml @@ -0,0 +1,18 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ../default + - metalnet-rbac + +patchesStrategicMerge: + - patch-controller.yaml + +patchesJson6902: + - target: + group: apps + version: v1 + kind: Deployment + namespace: metalnetlet-system + name: metalnetlet-controller-manager + path: add-manager-args.yaml diff --git a/config/metalnetlet/kind/metalnet-rbac/kustomization.yaml b/config/metalnetlet/kind/metalnet-rbac/kustomization.yaml new file mode 100644 index 00000000..2baaa209 --- /dev/null +++ b/config/metalnetlet/kind/metalnet-rbac/kustomization.yaml @@ -0,0 +1,17 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: metalnetlet-system +namePrefix: metalnetlet- + +resources: + - ../../metalnet-rbac + +patches: + - patch: | + $patch: delete + apiVersion: v1 + kind: ServiceAccount + metadata: + name: controller-manager + namespace: system diff --git a/config/metalnetlet/kind/patch-controller.yaml b/config/metalnetlet/kind/patch-controller.yaml new file mode 100644 index 00000000..3aff790f --- /dev/null +++ b/config/metalnetlet/kind/patch-controller.yaml @@ -0,0 +1,11 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + namespace: metalnetlet-system + name: metalnetlet-controller-manager +spec: + template: + spec: + containers: + - name: manager + imagePullPolicy: Never diff --git a/config/metalnetlet/manager/controller_manager_config.yaml b/config/metalnetlet/manager/controller_manager_config.yaml new file mode 100644 index 00000000..53752068 --- /dev/null +++ b/config/metalnetlet/manager/controller_manager_config.yaml @@ -0,0 +1,11 @@ +apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 +kind: ControllerManagerConfig +health: + healthProbeBindAddress: :8081 +metrics: + bindAddress: 127.0.0.1:8080 +webhook: + port: 9443 +leaderElection: + leaderElect: true + resourceName: ba861938.onmetal.de diff --git a/config/metalnetlet/manager/kustomization.yaml b/config/metalnetlet/manager/kustomization.yaml new file mode 100644 index 00000000..2bcd3eea --- /dev/null +++ b/config/metalnetlet/manager/kustomization.yaml @@ -0,0 +1,10 @@ +resources: +- manager.yaml + +generatorOptions: + disableNameSuffixHash: true + +configMapGenerator: +- name: manager-config + files: + - controller_manager_config.yaml diff --git a/config/metalnetlet/manager/manager.yaml b/config/metalnetlet/manager/manager.yaml new file mode 100644 index 00000000..dd0e15a2 --- /dev/null +++ b/config/metalnetlet/manager/manager.yaml @@ -0,0 +1,58 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + name: system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system + labels: + control-plane: controller-manager +spec: + selector: + matchLabels: + control-plane: controller-manager + replicas: 1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + spec: + securityContext: + runAsNonRoot: true + containers: + - command: + - /manager + args: + - --leader-elect + image: metalnetlet:latest + name: manager + securityContext: + allowPrivilegeEscalation: false + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 100m + memory: 30Mi + requests: + cpu: 100m + memory: 20Mi + serviceAccountName: controller-manager + terminationGracePeriodSeconds: 10 diff --git a/config/metalnetlet/metalnet-rbac/kustomization.yaml b/config/metalnetlet/metalnet-rbac/kustomization.yaml new file mode 100644 index 00000000..d1728d74 --- /dev/null +++ b/config/metalnetlet/metalnet-rbac/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: +- role.yaml +- role_binding.yaml +- service_account.yaml \ No newline at end of file diff --git a/config/metalnetlet/metalnet-rbac/role.yaml b/config/metalnetlet/metalnet-rbac/role.yaml new file mode 100644 index 00000000..a5ba2513 --- /dev/null +++ b/config/metalnetlet/metalnet-rbac/role.yaml @@ -0,0 +1,26 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + namespace: system + name: metalnet-role +rules: +- apiGroups: + - networking.metalnet.onmetal.de + resources: + - networks + verbs: + - get + - list + - patch + - update + - watch + - create + - delete + - deletecollection +- apiGroups: + - networking.metalnet.onmetal.de + resources: + - networks/finalizers + verbs: + - patch + - update diff --git a/config/metalnetlet/metalnet-rbac/role_binding.yaml b/config/metalnetlet/metalnet-rbac/role_binding.yaml new file mode 100644 index 00000000..6f63a119 --- /dev/null +++ b/config/metalnetlet/metalnet-rbac/role_binding.yaml @@ -0,0 +1,13 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + namespace: system + name: metalnet-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: metalnet-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/config/metalnetlet/metalnet-rbac/service_account.yaml b/config/metalnetlet/metalnet-rbac/service_account.yaml new file mode 100644 index 00000000..4669ee7d --- /dev/null +++ b/config/metalnetlet/metalnet-rbac/service_account.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + namespace: system + name: controller-manager diff --git a/config/metalnetlet/prometheus/kustomization.yaml b/config/metalnetlet/prometheus/kustomization.yaml new file mode 100644 index 00000000..ed137168 --- /dev/null +++ b/config/metalnetlet/prometheus/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- monitor.yaml diff --git a/config/metalnetlet/prometheus/monitor.yaml b/config/metalnetlet/prometheus/monitor.yaml new file mode 100644 index 00000000..d19136ae --- /dev/null +++ b/config/metalnetlet/prometheus/monitor.yaml @@ -0,0 +1,20 @@ + +# Prometheus Monitor Service (Metrics) +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + control-plane: controller-manager + name: controller-manager-metrics-monitor + namespace: system +spec: + endpoints: + - path: /metrics + port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + insecureSkipVerify: true + selector: + matchLabels: + control-plane: controller-manager diff --git a/config/metalnetlet/rbac/auth_proxy_client_clusterrole.yaml b/config/metalnetlet/rbac/auth_proxy_client_clusterrole.yaml new file mode 100644 index 00000000..51a75db4 --- /dev/null +++ b/config/metalnetlet/rbac/auth_proxy_client_clusterrole.yaml @@ -0,0 +1,9 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: metrics-reader +rules: +- nonResourceURLs: + - "/metrics" + verbs: + - get diff --git a/config/metalnetlet/rbac/auth_proxy_role.yaml b/config/metalnetlet/rbac/auth_proxy_role.yaml new file mode 100644 index 00000000..80e1857c --- /dev/null +++ b/config/metalnetlet/rbac/auth_proxy_role.yaml @@ -0,0 +1,17 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: proxy-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create diff --git a/config/metalnetlet/rbac/auth_proxy_role_binding.yaml b/config/metalnetlet/rbac/auth_proxy_role_binding.yaml new file mode 100644 index 00000000..ec7acc0a --- /dev/null +++ b/config/metalnetlet/rbac/auth_proxy_role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: proxy-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/config/metalnetlet/rbac/auth_proxy_service.yaml b/config/metalnetlet/rbac/auth_proxy_service.yaml new file mode 100644 index 00000000..6cf656be --- /dev/null +++ b/config/metalnetlet/rbac/auth_proxy_service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + name: controller-manager-metrics-service + namespace: system +spec: + ports: + - name: https + port: 8443 + targetPort: https + selector: + control-plane: controller-manager diff --git a/config/metalnetlet/rbac/kustomization.yaml b/config/metalnetlet/rbac/kustomization.yaml new file mode 100644 index 00000000..731832a6 --- /dev/null +++ b/config/metalnetlet/rbac/kustomization.yaml @@ -0,0 +1,18 @@ +resources: +# All RBAC will be applied under this service account in +# the deployment namespace. You may comment out this resource +# if your manager will use a service account that exists at +# runtime. Be sure to update RoleBinding and ClusterRoleBinding +# subjects if changing service account names. +- service_account.yaml +- role.yaml +- role_binding.yaml +- leader_election_role.yaml +- leader_election_role_binding.yaml +# Comment the following 4 lines if you want to disable +# the auth proxy (https://github.com/brancz/kube-rbac-proxy) +# which protects your /metrics endpoint. +- auth_proxy_service.yaml +- auth_proxy_role.yaml +- auth_proxy_role_binding.yaml +- auth_proxy_client_clusterrole.yaml diff --git a/config/metalnetlet/rbac/leader_election_role.yaml b/config/metalnetlet/rbac/leader_election_role.yaml new file mode 100644 index 00000000..4190ec80 --- /dev/null +++ b/config/metalnetlet/rbac/leader_election_role.yaml @@ -0,0 +1,37 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: leader-election-role +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch diff --git a/config/metalnetlet/rbac/leader_election_role_binding.yaml b/config/metalnetlet/rbac/leader_election_role_binding.yaml new file mode 100644 index 00000000..1d1321ed --- /dev/null +++ b/config/metalnetlet/rbac/leader_election_role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: leader-election-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: leader-election-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/config/metalnetlet/rbac/role.yaml b/config/metalnetlet/rbac/role.yaml new file mode 100644 index 00000000..20a04be6 --- /dev/null +++ b/config/metalnetlet/rbac/role.yaml @@ -0,0 +1,53 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: manager-role +rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - get + - list + - patch + - update + - watch +- apiGroups: + - apinet.api.onmetal.de + resources: + - networks + verbs: + - get + - list + - watch +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +- apiGroups: + - certificates.k8s.io + resources: + - certificatesigningrequests + verbs: + - create + - get + - list + - watch +- apiGroups: + - certificates.k8s.io + resources: + - certificatesigningrequests/metalnetletclient + verbs: + - create diff --git a/config/metalnetlet/rbac/role_binding.yaml b/config/metalnetlet/rbac/role_binding.yaml new file mode 100644 index 00000000..2070ede4 --- /dev/null +++ b/config/metalnetlet/rbac/role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: manager-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/config/metalnetlet/rbac/service_account.yaml b/config/metalnetlet/rbac/service_account.yaml new file mode 100644 index 00000000..7cd6025b --- /dev/null +++ b/config/metalnetlet/rbac/service_account.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: controller-manager + namespace: system diff --git a/config/onmetal-api-net/rbac/kustomization.yaml b/config/onmetal-api-net/rbac/kustomization.yaml index a2ee323d..819026f6 100644 --- a/config/onmetal-api-net/rbac/kustomization.yaml +++ b/config/onmetal-api-net/rbac/kustomization.yaml @@ -21,3 +21,8 @@ resources: - apinetlet_rolebinding.yaml - apinetlet_bootstrapper_role.yaml - apinetlet_bootstrapper_rolebinding.yaml +# Metalnetlet (bootstrapper) roles +- metalnetlet_role.yaml +- metalnetlet_rolebinding.yaml +- metalnetlet_bootstrapper_role.yaml +- metalnetlet_bootstrapper_rolebinding.yaml diff --git a/config/onmetal-api-net/rbac/metalnetlet_bootstrapper_role.yaml b/config/onmetal-api-net/rbac/metalnetlet_bootstrapper_role.yaml new file mode 100644 index 00000000..e80b30fb --- /dev/null +++ b/config/onmetal-api-net/rbac/metalnetlet_bootstrapper_role.yaml @@ -0,0 +1,20 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: apinet.api.onmetal.de:system:metalnetlet-bootstrapper +rules: + - apiGroups: + - certificates.k8s.io + resources: + - certificatesigningrequests + verbs: + - create + - get + - list + - watch + - apiGroups: + - certificates.k8s.io + resources: + - certificatesigningrequests/metalnetletclient + verbs: + - create \ No newline at end of file diff --git a/config/onmetal-api-net/rbac/metalnetlet_bootstrapper_rolebinding.yaml b/config/onmetal-api-net/rbac/metalnetlet_bootstrapper_rolebinding.yaml new file mode 100644 index 00000000..d5aa823a --- /dev/null +++ b/config/onmetal-api-net/rbac/metalnetlet_bootstrapper_rolebinding.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: apinet.api.onmetal.de:system:metalnetlet-bootstrapper +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: apinet.api.onmetal.de:system:metalnetlet-bootstrapper +subjects: + - kind: Group + # Group name has to match bootstrap group pattern \Asystem:bootstrappers:[a-z0-9:-]{0,255}[a-z0-9]\ + # See https://github.com/kubernetes/kubernetes/blob/e8662a46dd27db774ec953dae15f93ae2d1a68c8/staging/src/k8s.io/cluster-bootstrap/token/api/types.go#L96 + name: system:bootstrappers:apinet-api-onmetal-de:metalnetlets + apiGroup: rbac.authorization.k8s.io diff --git a/config/onmetal-api-net/rbac/metalnetlet_role.yaml b/config/onmetal-api-net/rbac/metalnetlet_role.yaml new file mode 100644 index 00000000..8eb2bd74 --- /dev/null +++ b/config/onmetal-api-net/rbac/metalnetlet_role.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: apinet.api.onmetal.de:system:metalnetlets +rules: +- apiGroups: + - apinet.api.onmetal.de + resources: + - networks + verbs: + - get + - list + - patch + - update + - watch + - create + - delete diff --git a/config/onmetal-api-net/rbac/metalnetlet_rolebinding.yaml b/config/onmetal-api-net/rbac/metalnetlet_rolebinding.yaml new file mode 100644 index 00000000..c17a00bd --- /dev/null +++ b/config/onmetal-api-net/rbac/metalnetlet_rolebinding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: apinet.api.onmetal.de:system:metalnetlets +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: apinet.api.onmetal.de:system:metalnetlets +subjects: + - kind: Group + name: apinet.api.onmetal.de:system:metalnetlets + apiGroup: rbac.authorization.k8s.io diff --git a/go.mod b/go.mod index 3b026c82..197c61d1 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.20 require ( github.com/go-logr/logr v1.2.4 github.com/onmetal/controller-utils v0.7.0 + github.com/onmetal/metalnet v0.0.0-20230406114910-a3ad7cdb7485 github.com/onmetal/onmetal-api v0.1.1 github.com/onsi/ginkgo/v2 v2.9.5 github.com/onsi/gomega v1.27.7 diff --git a/go.sum b/go.sum index e7781617..60a0f613 100644 --- a/go.sum +++ b/go.sum @@ -255,6 +255,8 @@ github.com/onmetal/controller-utils v0.7.0 h1:EHLPb/XimNas1VkeZZLP4g31aSz+ipiwvw github.com/onmetal/controller-utils v0.7.0/go.mod h1:91KV/s0VaB8PC+hqsxo6OBsAhi3ICFgIFLv/36V0ng8= github.com/onmetal/onmetal-api v0.1.1 h1:8YEtQBsxQzb+MKvbFypuYp/x8AZ96mN4NCx8bRxHvFA= github.com/onmetal/onmetal-api v0.1.1/go.mod h1:mql3gEgyCDeqkQbeM62C/FRuCwXSfBVsvg/FiBczN6M= +github.com/onmetal/metalnet v0.0.0-20230406114910-a3ad7cdb7485 h1:xJ2+j6YwqB4jYXiqjui3buRzM+gVgZpzKxcbphJ15UI= +github.com/onmetal/metalnet v0.0.0-20230406114910-a3ad7cdb7485/go.mod h1:rwiKaGrmO4T3PERqHFeZNxjQNzGr4plamAJWgIftK04= github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU= diff --git a/hack/setup-git-redirect.sh b/hack/setup-git-redirect.sh new file mode 100755 index 00000000..e46075c8 --- /dev/null +++ b/hack/setup-git-redirect.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -e + +if [[ -f "$GITHUB_PAT_PATH" ]]; then + echo "Sourcing Github pat from path" + GITHUB_PAT="$(cat "$GITHUB_PAT_PATH")" +fi + +if [[ "$GITHUB_PAT" != "" ]]; then + echo "Rewriting to use Github pat" + git config --global url."https://${GITHUB_PAT}:x-oauth-basic@github.com/".insteadOf "https://github.com/" +else + echo "No Github pat given, rewriting to use plain ssh auth" + git config --global url."git@github.com:".insteadOf "https://github.com" +fi diff --git a/metalnetlet/client/config/getter.go b/metalnetlet/client/config/getter.go new file mode 100644 index 00000000..029d4bb4 --- /dev/null +++ b/metalnetlet/client/config/getter.go @@ -0,0 +1,54 @@ +// Copyright 2023 OnMetal authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "crypto/x509" + "crypto/x509/pkix" + "os" + + onmetalapinetv1alpha1 "github.com/onmetal/onmetal-api-net/api/v1alpha1" + utilcertificate "github.com/onmetal/onmetal-api/utils/certificate" + "github.com/onmetal/onmetal-api/utils/client/config" + certificatesv1 "k8s.io/api/certificates/v1" + "k8s.io/apiserver/pkg/server/egressselector" + ctrl "sigs.k8s.io/controller-runtime" +) + +var log = ctrl.Log.WithName("client").WithName("config") + +func NewGetter(name string) (*config.Getter, error) { + return config.NewGetter(config.GetterOptions{ + Name: "metalnetlet", + SignerName: certificatesv1.KubeAPIServerClientSignerName, + Template: &x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: onmetalapinetv1alpha1.MetalnetletCommonName(name), + Organization: []string{onmetalapinetv1alpha1.MetalnetletsGroup}, + }, + }, + GetUsages: utilcertificate.DefaultKubeAPIServerClientGetUsages, + NetworkContext: egressselector.ControlPlane.AsNetworkContext(), + }) +} + +func NewGetterOrDie(name string) *config.Getter { + getter, err := NewGetter(name) + if err != nil { + log.Error(err, "Error creating getter") + os.Exit(1) + } + return getter +} diff --git a/metalnetlet/controllers/controllers_suite_test.go b/metalnetlet/controllers/controllers_suite_test.go new file mode 100644 index 00000000..4874e2f6 --- /dev/null +++ b/metalnetlet/controllers/controllers_suite_test.go @@ -0,0 +1,139 @@ +/* +Copyright 2022 The Metal Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "path/filepath" + "testing" + "time" + + metalnetv1alpha1 "github.com/onmetal/metalnet/api/v1alpha1" + "github.com/onmetal/onmetal-api-net/api/v1alpha1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + . "sigs.k8s.io/controller-runtime/pkg/envtest/komega" + logf "sigs.k8s.io/controller-runtime/pkg/log" + //+kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment + +const ( + pollingInterval = 50 * time.Millisecond + eventuallyTimeout = 3 * time.Second + consistentlyDuration = 1 * time.Second +) + +const ( + metalnetletName = "test-metalnetlet" +) + +func TestControllers(t *testing.T) { + SetDefaultConsistentlyPollingInterval(pollingInterval) + SetDefaultEventuallyPollingInterval(pollingInterval) + SetDefaultEventuallyTimeout(eventuallyTimeout) + SetDefaultConsistentlyDuration(consistentlyDuration) + + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(GinkgoLogr) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{ + filepath.Join("..", "..", "config", "onmetal-api-net", "crd", "bases"), + filepath.Join("testdata", "metalnet-crds"), + }, + ErrorIfCRDPathMissing: true, + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + DeferCleanup(testEnv.Stop) + + Expect(v1alpha1.AddToScheme(scheme.Scheme)).To(Succeed()) + Expect(metalnetv1alpha1.AddToScheme(scheme.Scheme)).To(Succeed()) + + //+kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + SetClient(k8sClient) + + k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + Host: "127.0.0.1", + MetricsBindAddress: "0", + }) + Expect(err).ToNot(HaveOccurred()) + + // register reconciler here + Expect((&NetworkReconciler{ + Client: k8sManager.GetClient(), + MetalnetCluster: k8sManager, + Name: metalnetletName, + }).SetupWithManager(k8sManager)).To(Succeed()) + + mgrCtx, cancel := context.WithCancel(context.Background()) + DeferCleanup(cancel) + go func() { + defer GinkgoRecover() + Expect(k8sManager.Start(mgrCtx)).To(Succeed(), "failed to start manager") + }() +}) + +func SetupTest() *corev1.Namespace { + ns := &corev1.Namespace{} + + BeforeEach(func(ctx SpecContext) { + + *ns = corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "testns-", + }, + } + Expect(k8sClient.Create(ctx, ns)).To(Succeed(), "failed to create test namespace") + + DeferCleanup(func(ctx context.Context) { + Expect(k8sClient.Delete(ctx, ns)).To(Succeed(), "failed to delete test namespace") + }) + }) + + return ns +} diff --git a/metalnetlet/controllers/network_controller.go b/metalnetlet/controllers/network_controller.go new file mode 100644 index 00000000..f2f70765 --- /dev/null +++ b/metalnetlet/controllers/network_controller.go @@ -0,0 +1,225 @@ +// Copyright 2023 OnMetal authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controllers + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" + "github.com/onmetal/controller-utils/clientutils" + metalnetv1alpha1 "github.com/onmetal/metalnet/api/v1alpha1" + "github.com/onmetal/onmetal-api-net/api/v1alpha1" + "github.com/onmetal/onmetal-api-net/apiutils" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/cluster" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +const ( + metalnetNetworkFieldOwner = client.FieldOwner("metalnetlet.apinet.onmetal.de/field-owner") + networkNamespaceLabel = "metalnetlet.apinet.onmetal.de/network-namespace" + networkNameLabel = "metalnetlet.apinet.onmetal.de/network-name" + networkUIDLabel = "metalnet.apinet.onmetal.de/network-uid" +) + +func finalizer(name string) string { + return fmt.Sprintf("%s.metalnetlet.apinet.onmetal.de/network", name) +} + +func getNetworkVNI(network *v1alpha1.Network) (int32, bool) { + if !apiutils.IsNetworkAllocated(network) { + return 0, false + } + vni := network.Spec.VNI + if vni == nil { + return 0, false + } + return *vni, true +} + +type NetworkReconciler struct { + client.Client + MetalnetCluster cluster.Cluster + Name string +} + +//+kubebuilder:rbac:groups=apinet.api.onmetal.de,resources=networks,verbs=get;list;watch + +func (r *NetworkReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := ctrl.LoggerFrom(ctx) + network := &v1alpha1.Network{} + if err := r.Get(ctx, req.NamespacedName, network); err != nil { + if !apierrors.IsNotFound(err) { + return ctrl.Result{}, fmt.Errorf("error getting network: %w", err) + } + return r.deleteGone(ctx, log, req.NamespacedName) + } + + return r.reconcileExists(ctx, log, network) +} + +func (r *NetworkReconciler) deleteGone(ctx context.Context, log logr.Logger, networkKey client.ObjectKey) (ctrl.Result, error) { + log.V(1).Info("Delete gone") + + log.V(1).Info("Deleting all metalnet networks that match the network label") + if err := r.MetalnetCluster.GetClient().DeleteAllOf(ctx, &metalnetv1alpha1.Network{}, + client.MatchingLabels{ + networkNamespaceLabel: networkKey.Namespace, + networkNameLabel: networkKey.Name, + }, + ); err != nil { + return ctrl.Result{}, fmt.Errorf("error deleting metalnet networks matching network label: %w", err) + } + + log.V(1).Info("Deleted gone") + return ctrl.Result{}, nil +} + +func (r *NetworkReconciler) reconcileExists(ctx context.Context, log logr.Logger, network *v1alpha1.Network) (ctrl.Result, error) { + if !network.DeletionTimestamp.IsZero() { + return r.delete(ctx, log, network) + } + return r.reconcile(ctx, log, network) +} + +func (r *NetworkReconciler) delete(ctx context.Context, log logr.Logger, network *v1alpha1.Network) (ctrl.Result, error) { + log.V(1).Info("Delete") + + if !controllerutil.ContainsFinalizer(network, finalizer(r.Name)) { + log.V(1).Info("No finalizer present, nothing to do") + return ctrl.Result{}, nil + } + + log.V(1).Info("Finalizer present, cleaning up") + + vni := *network.Spec.VNI + + log.V(1).Info("Deleting metalnet network if present") + metalnetNetwork := &metalnetv1alpha1.Network{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("network-%d", vni), + }, + } + err := r.MetalnetCluster.GetClient().Delete(ctx, metalnetNetwork) + if err != nil && !apierrors.IsNotFound(err) { + return ctrl.Result{}, fmt.Errorf("error deleting metalnet network: %w", err) + } + if err == nil { + log.V(1).Info("Issued deletion of metalnet network") + return ctrl.Result{Requeue: true}, nil + } + + log.V(1).Info("Metalnet network is gone, removing finalizer") + if err := clientutils.PatchRemoveFinalizer(ctx, r.Client, network, finalizer(r.Name)); err != nil { + return ctrl.Result{}, fmt.Errorf("error removing finalizer: %w", err) + } + log.V(1).Info("Removed finalizer") + + log.V(1).Info("Deleted") + return ctrl.Result{}, nil +} + +func (r *NetworkReconciler) reconcile(ctx context.Context, log logr.Logger, network *v1alpha1.Network) (ctrl.Result, error) { + log.V(1).Info("Reconcile") + + vni, ok := getNetworkVNI(network) + if !ok { + log.V(1).Info("Network is not yet allocated") + return ctrl.Result{}, nil + } + + log.V(1).Info("Ensuring finalizer") + modified, err := clientutils.PatchEnsureFinalizer(ctx, r.Client, network, finalizer(r.Name)) + if err != nil { + return ctrl.Result{}, fmt.Errorf("error ensuring finalizer: %w", err) + } + if modified { + log.V(1).Info("Added finalizer") + return ctrl.Result{Requeue: true}, nil + } + log.V(1).Info("Finalizer is present") + + log.V(1).Info("Applying metalnet network") + metalnetNetwork := &metalnetv1alpha1.Network{ + TypeMeta: metav1.TypeMeta{ + APIVersion: metalnetv1alpha1.GroupVersion.String(), + Kind: "Network", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("network-%d", vni), + Labels: map[string]string{ + networkNamespaceLabel: network.Namespace, + networkNameLabel: network.Name, + networkUIDLabel: string(network.UID), + }, + }, + Spec: metalnetv1alpha1.NetworkSpec{ + ID: vni, + PeeredIDs: network.Spec.PeerVNIs, + }, + } + if err := r.MetalnetCluster.GetClient().Patch(ctx, metalnetNetwork, client.Apply, metalnetNetworkFieldOwner); err != nil { + return ctrl.Result{}, fmt.Errorf("error applying network: %w", err) + } + log.V(1).Info("Applied metalnet network") + + log.V(1).Info("Reconciled") + return ctrl.Result{}, nil +} + +func (r *NetworkReconciler) enqueueNetworkUsingNetworkLabels() handler.EventHandler { + return handler.EnqueueRequestsFromMapFunc(func(obj client.Object) []ctrl.Request { + metalnetNetwork := obj.(*metalnetv1alpha1.Network) + networkNamespace, networkName := metalnetNetwork.Labels[networkNamespaceLabel], metalnetNetwork.Labels[networkNameLabel] + if networkNamespace == "" || networkName == "" { + return nil + } + return []ctrl.Request{ + { + NamespacedName: client.ObjectKey{ + Namespace: networkNamespace, + Name: networkName, + }, + }, + } + }) +} + +var networkHasVNI = predicate.NewPredicateFuncs(func(obj client.Object) bool { + network := obj.(*v1alpha1.Network) + _, ok := getNetworkVNI(network) + return ok +}) + +func (r *NetworkReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For( + &v1alpha1.Network{}, + builder.WithPredicates(networkHasVNI), + ). + Watches( + source.NewKindWithCache(&metalnetv1alpha1.Network{}, r.MetalnetCluster.GetCache()), + r.enqueueNetworkUsingNetworkLabels(), + ). + Complete(r) +} diff --git a/metalnetlet/controllers/network_controller_test.go b/metalnetlet/controllers/network_controller_test.go new file mode 100644 index 00000000..726e8e4c --- /dev/null +++ b/metalnetlet/controllers/network_controller_test.go @@ -0,0 +1,66 @@ +// Copyright 2022 OnMetal authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controllers + +import ( + metalnetv1alpha1 "github.com/onmetal/metalnet/api/v1alpha1" + "github.com/onmetal/onmetal-api-net/api/v1alpha1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ = Describe("NetworkController", func() { + ns := SetupTest() + + It("should create a metalnet network for a network", func(ctx SpecContext) { + By("creating a network") + network := &v1alpha1.Network{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + GenerateName: "network-", + }, + Spec: v1alpha1.NetworkSpec{ + VNI: pointer.Int32(300), + }, + } + Expect(k8sClient.Create(ctx, network)).To(Succeed()) + + By("patching the network to be available") + baseNetwork := network.DeepCopy() + network.Status.Conditions = []v1alpha1.NetworkCondition{ + { + Type: v1alpha1.NetworkAllocated, + Status: corev1.ConditionTrue, + Reason: "Allocated", + Message: "The network has been allocated.", + }, + } + Expect(k8sClient.Status().Patch(ctx, network, client.MergeFrom(baseNetwork))).Should(Succeed()) + + By("waiting for the metalnet network to be created") + metalnetNetwork := &metalnetv1alpha1.Network{} + metalnetNetworkKey := client.ObjectKey{Name: "network-300"} + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, metalnetNetworkKey, metalnetNetwork)).To(Succeed()) + g.Expect(metalnetNetwork.Spec).To(Equal(metalnetv1alpha1.NetworkSpec{ + ID: 300, + })) + }).Should(Succeed()) + }) +}) diff --git a/metalnetlet/controllers/rbac.go b/metalnetlet/controllers/rbac.go new file mode 100644 index 00000000..532f48a8 --- /dev/null +++ b/metalnetlet/controllers/rbac.go @@ -0,0 +1,26 @@ +// Copyright 2023 OnMetal authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controllers + +// Additional required RBAC rules + +// Rules required for kubeconfig-rotation +//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch +//+kubebuilder:rbac:groups=certificates.k8s.io,resources=certificatesigningrequests,verbs=create;get;list;watch +//+kubebuilder:rbac:groups=certificates.k8s.io,resources=certificatesigningrequests/metalnetletclient,verbs=create + +// Rules required for delegated authentication +//+kubebuilder:rbac:groups=authentication.k8s.io,resources=tokenreviews,verbs=create +//+kubebuilder:rbac:groups=authorization.k8s.io,resources=subjectaccessreviews,verbs=create diff --git a/metalnetlet/controllers/testdata/metalnet-crds/network.yaml b/metalnetlet/controllers/testdata/metalnet-crds/network.yaml new file mode 100644 index 00000000..4377b684 --- /dev/null +++ b/metalnetlet/controllers/testdata/metalnet-crds/network.yaml @@ -0,0 +1,67 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.0 + creationTimestamp: null + name: networks.networking.metalnet.onmetal.de +spec: + group: networking.metalnet.onmetal.de + names: + kind: Network + listKind: NetworkList + plural: networks + singular: network + scope: Cluster + versions: + - additionalPrinterColumns: + - description: ID of the network. + jsonPath: .spec.id + name: Handle + priority: 10 + type: integer + - description: Age of the network. + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: Network is the Schema for the networks API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: NetworkSpec defines the desired state of Network + properties: + id: + description: ID is the unique identifier of the Network + format: int32 + maximum: 16777215 + minimum: 1 + type: integer + peeredIDs: + description: PeeredIDs are the IDs of networks to peer with. + items: + format: int32 + type: integer + type: array + required: + - id + type: object + type: object + served: true + storage: true + subresources: + status: {} \ No newline at end of file diff --git a/metalnetlet/main.go b/metalnetlet/main.go new file mode 100644 index 00000000..d31f204a --- /dev/null +++ b/metalnetlet/main.go @@ -0,0 +1,164 @@ +/* +Copyright 2021. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + goflag "flag" + "fmt" + "os" + + "github.com/onmetal/controller-utils/configutils" + metalnetv1alpha1 "github.com/onmetal/metalnet/api/v1alpha1" + "github.com/onmetal/onmetal-api-net/api/v1alpha1" + metalnetletconfig "github.com/onmetal/onmetal-api-net/metalnetlet/client/config" + "github.com/onmetal/onmetal-api-net/metalnetlet/controllers" + "github.com/onmetal/onmetal-api/utils/client/config" + flag "github.com/spf13/pflag" + "sigs.k8s.io/controller-runtime/pkg/cluster" + + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) + // to ensure that exec-entrypoint and run can make use of them. + _ "k8s.io/client-go/plugin/pkg/client/auth" + + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + //+kubebuilder:scaffold:imports +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + + utilruntime.Must(v1alpha1.AddToScheme(scheme)) + utilruntime.Must(metalnetv1alpha1.AddToScheme(scheme)) + + //+kubebuilder:scaffold:scheme +} + +func main() { + var name string + + var metricsAddr string + var enableLeaderElection bool + var probeAddr string + + var configOptions config.GetConfigOptions + var metalnetKubeconfig string + + flag.StringVar(&name, "name", "", "The name of the partition the metalnetlet represents (required).") + flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") + flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + flag.BoolVar(&enableLeaderElection, "leader-elect", false, + "Enable leader election for controller manager. "+ + "Enabling this will ensure there is only one active controller manager.") + + configOptions.BindFlags(flag.CommandLine) + flag.StringVar(&metalnetKubeconfig, "metalnet-kubeconfig", "", "Metalnet kubeconfig to use.") + + opts := zap.Options{ + Development: true, + } + goFlags := goflag.NewFlagSet(os.Args[0], goflag.ExitOnError) + opts.BindFlags(goFlags) + flag.CommandLine.AddGoFlagSet(goFlags) + flag.Parse() + + ctx := ctrl.SetupSignalHandler() + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + if name == "" { + setupLog.Error(fmt.Errorf("must specify name"), "invalid configuration") + os.Exit(1) + } + + getter := metalnetletconfig.NewGetterOrDie(name) + cfg, cfgCtrl, err := getter.GetConfig(ctx, &configOptions) + if err != nil { + setupLog.Error(err, "unable to load kubeconfig") + os.Exit(1) + } + + metalnetCfg, err := configutils.GetConfig(configutils.Kubeconfig(metalnetKubeconfig)) + if err != nil { + setupLog.Error(err, "unable to load api net kubeconfig") + os.Exit(1) + } + + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme, + MetricsBindAddress: metricsAddr, + Port: 9443, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "bf12dae0.metalnetlet.apinet.api.onmetal.de", + }) + if err != nil { + setupLog.Error(err, "unable to start manager") + os.Exit(1) + } + if err := config.SetupControllerWithManager(mgr, cfgCtrl); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Config") + os.Exit(1) + } + + metalnetCluster, err := cluster.New(metalnetCfg, func(options *cluster.Options) { + options.Scheme = scheme + }) + if err != nil { + setupLog.Error(err, "unable to create metalnet cluster") + os.Exit(1) + } + + if err := mgr.Add(metalnetCluster); err != nil { + setupLog.Error(err, "unable to add cluster", "cluster", "APINet") + os.Exit(1) + } + + if err := (&controllers.NetworkReconciler{ + Client: mgr.GetClient(), + MetalnetCluster: metalnetCluster, + Name: name, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Networking") + os.Exit(1) + } + //+kubebuilder:scaffold:builder + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up health check") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up ready check") + os.Exit(1) + } + + setupLog.Info("starting manager") + if err := mgr.Start(ctx); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } +} diff --git a/onmetal-api-net/controllers/certificate/onmetal-api-net/metalnetlet.go b/onmetal-api-net/controllers/certificate/onmetal-api-net/metalnetlet.go new file mode 100644 index 00000000..6590aa36 --- /dev/null +++ b/onmetal-api-net/controllers/certificate/onmetal-api-net/metalnetlet.go @@ -0,0 +1,90 @@ +// Copyright 2023 OnMetal authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package onmetalapinet + +import ( + "crypto/x509" + "fmt" + "strings" + + onmetalapinetv1alpha1 "github.com/onmetal/onmetal-api-net/api/v1alpha1" + "github.com/onmetal/onmetal-api-net/onmetal-api-net/controllers/certificate/generic" + "golang.org/x/exp/slices" + authv1 "k8s.io/api/authorization/v1" + certificatesv1 "k8s.io/api/certificates/v1" + "k8s.io/apimachinery/pkg/util/sets" +) + +var ( + MetalnetletRequiredUsages = sets.New[certificatesv1.KeyUsage]( + certificatesv1.UsageDigitalSignature, + certificatesv1.UsageKeyEncipherment, + certificatesv1.UsageClientAuth, + ) +) + +func IsMetalnetletClientCert(csr *certificatesv1.CertificateSigningRequest, x509cr *x509.CertificateRequest) bool { + if csr.Spec.SignerName != certificatesv1.KubeAPIServerClientSignerName { + return false + } + + return ValidateMetalnetletClientCSR(x509cr, sets.New(csr.Spec.Usages...)) == nil +} + +func ValidateMetalnetletClientCSR(req *x509.CertificateRequest, usages sets.Set[certificatesv1.KeyUsage]) error { + if !slices.Equal([]string{onmetalapinetv1alpha1.MetalnetletsGroup}, req.Subject.Organization) { + return fmt.Errorf("organization is not %s", onmetalapinetv1alpha1.MetalnetletsGroup) + } + + if len(req.DNSNames) > 0 { + return fmt.Errorf("dns subject alternative names are not allowed") + } + if len(req.EmailAddresses) > 0 { + return fmt.Errorf("email subject alternative names are not allowed") + } + if len(req.IPAddresses) > 0 { + return fmt.Errorf("ip subject alternative names are not allowed") + } + if len(req.URIs) > 0 { + return fmt.Errorf("uri subject alternative names are not allowed") + } + + if !strings.HasPrefix(req.Subject.CommonName, onmetalapinetv1alpha1.MetalnetletUserNamePrefix) { + return fmt.Errorf("subject common name does not begin with %s", onmetalapinetv1alpha1.MetalnetletUserNamePrefix) + } + + if !MetalnetletRequiredUsages.Equal(usages) { + return fmt.Errorf("usages did not match %v", sets.List(MetalnetletRequiredUsages)) + } + + return nil +} + +var ( + MetalnetletRecognizer = generic.NewCertificateSigningRequestRecognizer( + IsMetalnetletClientCert, + authv1.ResourceAttributes{ + Group: certificatesv1.GroupName, + Resource: "certificatesigningrequests", + Verb: "create", + Subresource: "metalnetletclient", + }, + "Auto approving metalnetlet client certificate after SubjectAccessReview.", + ) +) + +func init() { + Recognizers = append(Recognizers, MetalnetletRecognizer) +}