Migrating from OperatorSDK 0.18 to 0.19 (#23)

This commit is contained in:
Dario Tranchitella
2020-08-04 16:30:28 +02:00
committed by GitHub
parent aab0d9b657
commit 5d20d515a7
110 changed files with 2786 additions and 3366 deletions

View File

@@ -35,7 +35,7 @@ A clear and concise description of what you expected to happen.
If applicable, please provide logs of `capsule`.
In a standard stand-alone installation of Capsule,
you'd get this by running `kubectl -n capsule-system logs deploy/capsule`.
you'd get this by running `kubectl -n capsule-system logs deploy/capsule-controller-manager`.
# Additional context

86
.gitignore vendored
View File

@@ -1,78 +1,26 @@
# Temporary Build Files
build/_output
build/_test
# Created by https://www.gitignore.io/api/go,vim,emacs,visualstudiocode
### Emacs ###
# -*- mode: gitignore; -*-
*~
\#*\#
/.emacs.desktop
/.emacs.desktop.lock
*.elc
auto-save-list
tramp
.\#*
# Org-mode
.org-id-locations
*_archive
# flymake-mode
*_flymake.*
# eshell files
/eshell/history
/eshell/lastdir
# elpa packages
/elpa/
# reftex files
*.rel
# AUCTeX auto folder
/auto/
# cask packages
.cask/
dist/
# Flycheck
flycheck_*.el
# server auth directory
/server/
# projectiles files
.projectile
projectile-bookmarks.eld
# directory configuration
.dir-locals.el
# saveplace
places
# url cache
url/cache/
# cedet
ede-projects.el
# smex
smex-items
# company-statistics
company-statistics-cache.el
# anaconda-mode
anaconda-mode/
### Go ###
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, build with 'go test -c'
bin
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
### Vim ###
# swap
.sw[a-p]
.*.sw[a-p]
# session
Session.vim
# temporary
.netrwhist
# auto-generated tag files
tags
### VisualStudioCode ###
.vscode/*
.history
# End of https://www.gitignore.io/api/go,vim,emacs,visualstudiocode
.idea
# Kubernetes Generated files - skip generated files, except for vendored files
!vendor/**/zz_generated.*
# editor and IDE paraphernalia
.idea
*.swp
*.swo
*~
hack/*.kubeconfig

31
Dockerfile Normal file
View File

@@ -0,0 +1,31 @@
# Build the manager binary
FROM golang:1.13 as builder
WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
# 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
# Copy the go source
COPY main.go main.go
COPY api/ api/
COPY controllers/ controllers/
COPY pkg/ pkg/
COPY version/ version/
ARG VERSION
# Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -ldflags "-X github.com/clastix/capsule/version.Version=${VERSION}" -o manager main.go
# 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
WORKDIR /
COPY --from=builder /workspace/manager .
USER nonroot:nonroot
ENTRYPOINT ["/manager"]

129
Makefile
View File

@@ -1,19 +1,118 @@
.PHONY: k8s
k8s:
operator-sdk generate k8s
# Current Operator version
VERSION ?= 0.0.1
.PHONY: crds
crds:
operator-sdk generate crds
# Default bundle image tag
BUNDLE_IMG ?= quay.io/clastix/capsule:$(VERSION)-bundle
# Options for 'bundle-build'
ifneq ($(origin CHANNELS), undefined)
BUNDLE_CHANNELS := --channels=$(CHANNELS)
endif
ifneq ($(origin DEFAULT_CHANNEL), undefined)
BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL)
endif
BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL)
.PHONY: docker-image
docker-image:
operator-sdk build quay.io/clastix/capsule:latest
# Image URL to use all building/pushing image targets
IMG ?= quay.io/clastix/capsule:latest
# Produce CRDs that work back to Kubernetes 1.11 (no version conversion)
CRD_OPTIONS ?= "crd:trivialVersions=true"
.PHONY: goimports
goimports:
goimports -w -l -local "github.com/clastix/capsule" .
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
GOBIN=$(shell go env GOPATH)/bin
else
GOBIN=$(shell go env GOBIN)
endif
.PHONY: golint
golint:
golangci-lint run
all: manager
# Run tests
test: generate fmt vet manifests
go test ./... -coverprofile cover.out
# Build manager binary
manager: generate fmt vet
go build -o bin/manager main.go
# Run against the configured Kubernetes cluster in ~/.kube/config
run: generate fmt vet manifests
go run ./main.go
# Install CRDs into a cluster
install: manifests kustomize
$(KUSTOMIZE) build config/crd | kubectl apply -f -
# Uninstall CRDs from a cluster
uninstall: manifests kustomize
$(KUSTOMIZE) build config/crd | kubectl delete -f -
# Deploy controller in the configured Kubernetes cluster in ~/.kube/config
deploy: manifests kustomize
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
$(KUSTOMIZE) build config/default | kubectl apply -f -
# Generate manifests e.g. CRD, RBAC etc.
manifests: controller-gen
$(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
# Run go fmt against code
fmt:
go fmt ./...
# Run go vet against code
vet:
go vet ./...
# Generate code
generate: controller-gen
$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."
# Build the docker image
docker-build: test
docker build . --build-arg=VERSION=${VERSION} -t ${IMG}
# Push the docker image
docker-push:
docker push ${IMG}
# find or download controller-gen
# download controller-gen if necessary
controller-gen:
ifeq (, $(shell which controller-gen))
@{ \
set -e ;\
CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\
cd $$CONTROLLER_GEN_TMP_DIR ;\
go mod init tmp ;\
go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.3.0 ;\
rm -rf $$CONTROLLER_GEN_TMP_DIR ;\
}
CONTROLLER_GEN=$(GOBIN)/controller-gen
else
CONTROLLER_GEN=$(shell which controller-gen)
endif
kustomize:
ifeq (, $(shell which kustomize))
@{ \
set -e ;\
KUSTOMIZE_GEN_TMP_DIR=$$(mktemp -d) ;\
cd $$KUSTOMIZE_GEN_TMP_DIR ;\
go mod init tmp ;\
go get sigs.k8s.io/kustomize/kustomize/v3@v3.5.4 ;\
rm -rf $$KUSTOMIZE_GEN_TMP_DIR ;\
}
KUSTOMIZE=$(GOBIN)/kustomize
else
KUSTOMIZE=$(shell which kustomize)
endif
# Generate bundle manifests and metadata, then validate generated files.
bundle: manifests
operator-sdk generate kustomize manifests -q
kustomize build config/manifests | operator-sdk generate bundle -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS)
operator-sdk bundle validate ./bundle
# Build the bundle image.
bundle-build:
docker build -f bundle.Dockerfile -t $(BUNDLE_IMG) .

10
PROJECT Normal file
View File

@@ -0,0 +1,10 @@
domain: github.com/clastix/capsule
layout: go.kubebuilder.io/v2
repo: github.com/clastix/capsule
resources:
- group: capsule.clastix.io
kind: Tenant
version: v1alpha1
version: 3-alpha
plugins:
go.operator-sdk.io/v2-alpha: {}

View File

@@ -8,24 +8,30 @@ _Container-as-a-Service_ (CaaS) platforms.
# tl;dr; How to install
As a Cluster Admin, ensure the `capsule-system` Namespace is already there.
Ensure you have [`kustomize`](https://github.com/kubernetes-sigs/kustomize)
installed in your `PATH`:
```
# kubectl apply -f deploy
mutatingwebhookconfiguration.admissionregistration.k8s.io/capsule created
clusterrole.rbac.authorization.k8s.io/namespace:deleter created
clusterrole.rbac.authorization.k8s.io/namespace:provisioner created
clusterrolebinding.rbac.authorization.k8s.io/namespace:provisioner created
deployment.apps/capsule created
clusterrole.rbac.authorization.k8s.io/capsule created
clusterrolebinding.rbac.authorization.k8s.io/capsule-cluster-admin created
clusterrolebinding.rbac.authorization.k8s.io/capsule created
secret/capsule-ca created
secret/capsule-tls created
service/capsule created
serviceaccount/capsule created
# kubectl apply -f deploy/crds/capsule.clastix.io_tenants_crd.yaml
customresourcedefinition.apiextensions.k8s.io/tenants.capsule.clastix.io created
make deploy
# /home/prometherion/go/bin/controller-gen "crd:trivialVersions=true" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
# cd config/manager && /usr/local/bin/kustomize edit set image controller=quay.io/clastix/capsule:latest
# /usr/local/bin/kustomize build config/default | kubectl apply -f -
# namespace/capsule-system created
# customresourcedefinition.apiextensions.k8s.io/tenants.capsule.clastix.io created
# clusterrole.rbac.authorization.k8s.io/capsule-namespace:deleter created
# clusterrole.rbac.authorization.k8s.io/capsule-namespace:provisioner created
# clusterrole.rbac.authorization.k8s.io/capsule-proxy-role created
# clusterrole.rbac.authorization.k8s.io/capsule-metrics-reader created
# clusterrolebinding.rbac.authorization.k8s.io/capsule-manager-rolebinding created
# clusterrolebinding.rbac.authorization.k8s.io/capsule-namespace:provisioner created
# clusterrolebinding.rbac.authorization.k8s.io/capsule-proxy-rolebinding created
# secret/capsule-ca created
# secret/capsule-tls created
# service/capsule-controller-manager-metrics-service created
# service/capsule-webhook-service created
# deployment.apps/capsule-controller-manager created
# mutatingwebhookconfiguration.admissionregistration.k8s.io/capsule-mutating-webhook-configuration created
# validatingwebhookconfiguration.admissionregistration.k8s.io/capsule-validating-webhook-configuration created
```
## Webhooks and CA Bundle
@@ -64,11 +70,11 @@ All Tenant owner needs to be granted with a X.509 certificate with
## How to create a Tenant
Use the [scaffold Tenant](deploy/crds/capsule.clastix.io_v1alpha1_tenant_cr.yaml)
Use the [scaffold Tenant](config/samples/capsule_v1alpha1_tenant.yaml)
and simply apply as Cluster Admin.
```
# kubectl apply -f deploy/crds/capsule.clastix.io_v1alpha1_tenant_cr.yaml
# kubectl apply -f config/samples/capsule_v1alpha1_tenant.yaml
tenant.capsule.clastix.io/oil created
```

View File

@@ -1,9 +1,12 @@
/*
Copyright 2020 Clastix Labs.
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.
@@ -11,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha1
package domain
type SearchIn interface {
IsStringInList(value string) bool

View File

@@ -1,9 +1,12 @@
/*
Copyright 2020 Clastix Labs.
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.
@@ -11,10 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// NOTE: Boilerplate only. Ignore this file.
// Package v1alpha1 contains API Schema definitions for the capsule v1alpha1 API group
// +k8s:deepcopy-gen=package,register
// Package v1alpha1 contains API Schema definitions for the capsule.clastix.io v1alpha1 API group
// +kubebuilder:object:generate=true
// +groupName=capsule.clastix.io
package v1alpha1
@@ -24,9 +25,12 @@ import (
)
var (
// SchemeGroupVersion is group version used to register these objects
SchemeGroupVersion = schema.GroupVersion{Group: "capsule.clastix.io", Version: "v1alpha1"}
// GroupVersion is group version used to register these objects
GroupVersion = schema.GroupVersion{Group: "capsule.clastix.io", Version: "v1alpha1"}
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
// AddToScheme adds the types in this group-version to the given scheme.
AddToScheme = SchemeBuilder.AddToScheme
)

View File

@@ -1,9 +1,12 @@
/*
Copyright 2020 Clastix Labs.
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.

View File

@@ -1,9 +1,12 @@
/*
Copyright 2020 Clastix Labs.
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.

View File

@@ -1,9 +1,12 @@
/*
Copyright 2020 Clastix Labs.
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.

View File

@@ -1,9 +1,12 @@
/*
Copyright 2020 Clastix Labs.
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.

View File

@@ -1,9 +1,12 @@
/*
Copyright 2020 Clastix Labs.
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.

View File

@@ -1,9 +1,12 @@
/*
Copyright 2020 Clastix Labs.
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.

View File

@@ -1,9 +1,12 @@
/*
Copyright 2020 Clastix Labs.
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.
@@ -45,12 +48,13 @@ type TenantStatus struct {
Groups []string `json:"groups,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// Tenant is the Schema for the tenants API
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:path=tenants,scope=Cluster
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:printcolumn:name="Namespace quota",type="integer",JSONPath=".spec.namespaceQuota",description="The max amount of Namespaces can be created"
// +kubebuilder:printcolumn:name="Namespace count",type="integer",JSONPath=".status.size",description="The total amount of Namespaces in use"
// Tenant is the Schema for the tenants API
type Tenant struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
@@ -59,7 +63,7 @@ type Tenant struct {
Status TenantStatus `json:"status,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +kubebuilder:object:root=true
// TenantList contains a list of Tenant
type TenantList struct {

View File

@@ -1,13 +1,29 @@
// +build !ignore_autogenerated
// Code generated by operator-sdk. DO NOT EDIT.
/*
Copyright 2020 Clastix Labs.
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.
*/
// Code generated by controller-gen. DO NOT EDIT.
package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/networking/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
@@ -16,7 +32,6 @@ func (in IngressClassList) DeepCopyInto(out *IngressClassList) {
in := &in
*out = make(IngressClassList, len(*in))
copy(*out, *in)
return
}
}
@@ -36,7 +51,6 @@ func (in NamespaceList) DeepCopyInto(out *NamespaceList) {
in := &in
*out = make(NamespaceList, len(*in))
copy(*out, *in)
return
}
}
@@ -56,7 +70,6 @@ func (in StorageClassList) DeepCopyInto(out *StorageClassList) {
in := &in
*out = make(StorageClassList, len(*in))
copy(*out, *in)
return
}
}
@@ -77,7 +90,6 @@ func (in *Tenant) DeepCopyInto(out *Tenant) {
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Tenant.
@@ -110,7 +122,6 @@ func (in *TenantList) DeepCopyInto(out *TenantList) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantList.
@@ -172,7 +183,6 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantSpec.
@@ -203,7 +213,6 @@ func (in *TenantStatus) DeepCopyInto(out *TenantStatus) {
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantStatus.

View File

@@ -1,15 +0,0 @@
FROM registry.access.redhat.com/ubi8/ubi-minimal:latest
ENV OPERATOR=/usr/local/bin/capsule \
USER_UID=0 \
USER_NAME=capsule
# install operator binary
COPY build/_output/bin/capsule ${OPERATOR}
COPY build/bin /usr/local/bin
RUN /usr/local/bin/user_setup
ENTRYPOINT ["/usr/local/bin/entrypoint"]
USER ${USER_UID}

View File

@@ -1,3 +0,0 @@
#!/bin/sh -e
exec ${OPERATOR} $@

View File

@@ -1,11 +0,0 @@
#!/bin/sh
set -x
# ensure $HOME exists and is accessible by group 0 (we don't know what the runtime UID will be)
echo "${USER_NAME}:x:${USER_UID}:0:${USER_NAME} user:${HOME}:/sbin/nologin" >> /etc/passwd
mkdir -p "${HOME}"
chown "${USER_UID}:0" "${HOME}"
chmod ug+rwx "${HOME}"
# no need for this script to remain in the image after running
rm "$0"

View File

@@ -1,244 +0,0 @@
/*
Copyright 2020 Clastix Labs.
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 (
"context"
"errors"
"flag"
"fmt"
"os"
"runtime"
"strings"
"github.com/operator-framework/operator-sdk/pkg/k8sutil"
kubemetrics "github.com/operator-framework/operator-sdk/pkg/kube-metrics"
"github.com/operator-framework/operator-sdk/pkg/leader"
"github.com/operator-framework/operator-sdk/pkg/log/zap"
"github.com/operator-framework/operator-sdk/pkg/metrics"
sdkVersion "github.com/operator-framework/operator-sdk/version"
"github.com/spf13/pflag"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/intstr"
_ "k8s.io/client-go/plugin/pkg/client/auth"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client/config"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/manager/signals"
"github.com/clastix/capsule/pkg/apis"
"github.com/clastix/capsule/pkg/controller"
"github.com/clastix/capsule/pkg/indexer"
"github.com/clastix/capsule/pkg/webhook"
"github.com/clastix/capsule/version"
)
// Change below variables to serve metrics on different host or port.
var (
metricsHost = "0.0.0.0"
metricsPort int32 = 8383
operatorMetricsPort int32 = 8686
)
var log = logf.Log.WithName("cmd")
func printVersion() {
log.Info(fmt.Sprintf("Operator Version: %s", version.Version))
log.Info(fmt.Sprintf("Go Version: %s", runtime.Version()))
log.Info(fmt.Sprintf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH))
log.Info(fmt.Sprintf("Version of operator-sdk: %v", sdkVersion.Version))
}
func main() {
// Add the zap logger flag set to the CLI. The flag set must
// be added before calling pflag.Parse().
pflag.CommandLine.AddFlagSet(zap.FlagSet())
// Add flags registered by imported packages (e.g. glog and
// controller-runtime)
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
var v bool
pflag.BoolVarP(&v, "version", "v", false, "Print the Capsule version and exit")
pflag.Parse()
// Use a zap logr.Logger implementation. If none of the zap
// flags are configured (or if the zap flag set is not being
// used), this defaults to a production zap logger.
//
// The logger instantiated here can be changed to any logger
// implementing the logr.Logger interface. This logger will
// be propagated through the whole operator, generating
// uniform and structured logs.
logf.SetLogger(zap.Logger())
printVersion()
if v {
os.Exit(0)
}
namespace, err := k8sutil.GetWatchNamespace()
if err != nil {
log.Error(err, "Failed to get watch namespace")
os.Exit(1)
}
// Get a config to talk to the apiserver
cfg, err := config.GetConfig()
if err != nil {
log.Error(err, "")
os.Exit(1)
}
ctx := context.TODO()
// Become the leader before proceeding
err = leader.Become(ctx, "capsule-lock")
if err != nil {
log.Error(err, "")
os.Exit(1)
}
// Set default manager options
options := manager.Options{
Namespace: namespace,
MetricsBindAddress: fmt.Sprintf("%s:%d", metricsHost, metricsPort),
}
// Add support for MultiNamespace set in WATCH_NAMESPACE (e.g ns1,ns2)
// Note that this is not intended to be used for excluding namespaces, this is better done via a Predicate
// Also note that you may face performance issues when using this with a high number of namespaces.
// More Info: https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg/cache#MultiNamespacedCacheBuilder
if strings.Contains(namespace, ",") {
options.Namespace = ""
options.NewCache = cache.MultiNamespacedCacheBuilder(strings.Split(namespace, ","))
}
stop := signals.SetupSignalHandler()
// Create a new manager to provide shared dependencies and start components
mgr, err := manager.New(cfg, options)
if err != nil {
log.Error(err, "")
os.Exit(1)
}
log.Info("Registering Components.")
// Setup Scheme for all resources
if err := apis.AddToScheme(mgr.GetScheme()); err != nil {
log.Error(err, "")
os.Exit(1)
}
// Setup all Controllers
if err := controller.AddToManager(mgr); err != nil {
log.Error(err, "")
os.Exit(1)
}
// Setup all Webhooks
if err := webhook.AddToServer(mgr); err != nil {
log.Error(err, "")
os.Exit(1)
}
// Setup all Custom Indexers
if err := indexer.AddToManager(mgr); err != nil {
log.Error(err, "")
os.Exit(1)
}
// Add the Metrics Service
addMetrics(ctx, cfg)
log.Info("Starting the Cmd.")
// Start the Cmd
if err := mgr.Start(stop); err != nil {
log.Error(err, "Manager exited non-zero")
os.Exit(1)
}
}
// addMetrics will create the Services and Service Monitors to allow the operator export the metrics by using
// the Prometheus operator
func addMetrics(ctx context.Context, cfg *rest.Config) {
// Get the namespace the operator is currently deployed in.
operatorNs, err := k8sutil.GetOperatorNamespace()
if err != nil {
if errors.Is(err, k8sutil.ErrRunLocal) {
log.Info("Skipping CR metrics server creation; not running in a cluster.")
return
}
}
if err := serveCRMetrics(cfg, operatorNs); err != nil {
log.Info("Could not generate and serve custom resource metrics", "error", err.Error())
}
// Add to the below struct any other metrics ports you want to expose.
servicePorts := []v1.ServicePort{
{Port: metricsPort, Name: metrics.OperatorPortName, Protocol: v1.ProtocolTCP, TargetPort: intstr.IntOrString{Type: intstr.Int, IntVal: metricsPort}},
{Port: operatorMetricsPort, Name: metrics.CRPortName, Protocol: v1.ProtocolTCP, TargetPort: intstr.IntOrString{Type: intstr.Int, IntVal: operatorMetricsPort}},
}
// Create Service object to expose the metrics port(s).
service, err := metrics.CreateMetricsService(ctx, cfg, servicePorts)
if err != nil {
log.Info("Could not create metrics Service", "error", err.Error())
}
// CreateServiceMonitors will automatically create the prometheus-operator ServiceMonitor resources
// necessary to configure Prometheus to scrape metrics from this operator.
services := []*v1.Service{service}
// The ServiceMonitor is created in the same namespace where the operator is deployed
_, err = metrics.CreateServiceMonitors(cfg, operatorNs, services)
if err != nil {
log.Info("Could not create ServiceMonitor object", "error", err.Error())
// If this operator is deployed to a cluster without the prometheus-operator running, it will return
// ErrServiceMonitorNotPresent, which can be used to safely skip ServiceMonitor creation.
if err == metrics.ErrServiceMonitorNotPresent {
log.Info("Install prometheus-operator in your cluster to create ServiceMonitor objects", "error", err.Error())
}
}
}
// serveCRMetrics gets the Operator/CustomResource GVKs and generates metrics based on those types.
// It serves those metrics on "http://metricsHost:operatorMetricsPort".
func serveCRMetrics(cfg *rest.Config, operatorNs string) error {
// The function below returns a list of filtered operator/CR specific GVKs. For more control, override the GVK list below
// with your own custom logic. Note that if you are adding third party API schemas, probably you will need to
// customize this implementation to avoid permissions issues.
filteredGVK, err := k8sutil.GetGVKsFromAddToScheme(apis.AddToScheme)
if err != nil {
return err
}
// The metrics will be generated from the namespaces which are returned here.
// NOTE that passing nil or an empty list of namespaces in GenerateAndServeCRMetrics will result in an error.
ns, err := kubemetrics.GetNamespacesForMetrics(operatorNs)
if err != nil {
return err
}
// Generate and serve custom resource specific metrics.
err = kubemetrics.GenerateAndServeCRMetrics(cfg, ns, filteredGVK, metricsHost, operatorMetricsPort)
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,702 @@
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.2.5
creationTimestamp: null
name: tenants.capsule.clastix.io
spec:
additionalPrinterColumns:
- JSONPath: .spec.namespaceQuota
description: The max amount of Namespaces can be created
name: Namespace quota
type: integer
- JSONPath: .status.size
description: The total amount of Namespaces in use
name: Namespace count
type: integer
group: capsule.clastix.io
names:
kind: Tenant
listKind: TenantList
plural: tenants
singular: tenant
scope: Cluster
subresources:
status: {}
validation:
openAPIV3Schema:
description: Tenant is the Schema for the tenants 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: TenantSpec defines the desired state of Tenant
properties:
ingressClasses:
items:
type: string
type: array
limitRanges:
items:
description: LimitRangeSpec defines a min/max usage limit for resources
that match on kind.
properties:
limits:
description: Limits is the list of LimitRangeItem objects that
are enforced.
items:
description: LimitRangeItem defines a min/max usage limit for
any resource that matches on kind.
properties:
default:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: Default resource requirement limit value by
resource name if resource limit is omitted.
type: object
defaultRequest:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: DefaultRequest is the default resource requirement
request value by resource name if resource request is
omitted.
type: object
max:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: Max usage constraints on this kind by resource
name.
type: object
maxLimitRequestRatio:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: MaxLimitRequestRatio if specified, the named
resource must have a request and limit that are both non-zero
where limit divided by request is less than or equal to
the enumerated value; this represents the max burst for
the named resource.
type: object
min:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: Min usage constraints on this kind by resource
name.
type: object
type:
description: Type of resource that this limit applies to.
type: string
required:
- type
type: object
type: array
required:
- limits
type: object
type: array
namespaceQuota:
minimum: 1
type: integer
networkPolicies:
items:
description: NetworkPolicySpec provides the specification of a NetworkPolicy
properties:
egress:
description: List of egress rules to be applied to the selected
pods. Outgoing traffic is allowed if there are no NetworkPolicies
selecting the pod (and cluster policy otherwise allows the traffic),
OR if the traffic matches at least one egress rule across all
of the NetworkPolicy objects whose podSelector matches the pod.
If this field is empty then this NetworkPolicy limits all outgoing
traffic (and serves solely to ensure that the pods it selects
are isolated by default). This field is beta-level in 1.8
items:
description: NetworkPolicyEgressRule describes a particular
set of traffic that is allowed out of pods matched by a NetworkPolicySpec's
podSelector. The traffic must match both ports and to. This
type is beta-level in 1.8
properties:
ports:
description: List of destination ports for outgoing traffic.
Each item in this list is combined using a logical OR.
If this field is empty or missing, this rule matches all
ports (traffic not restricted by port). If this field
is present and contains at least one item, then this rule
allows traffic only if the traffic matches at least one
port in the list.
items:
description: NetworkPolicyPort describes a port to allow
traffic on
properties:
port:
anyOf:
- type: integer
- type: string
description: The port on the given protocol. This
can either be a numerical or named port on a pod.
If this field is not provided, this matches all
port names and numbers.
x-kubernetes-int-or-string: true
protocol:
description: The protocol (TCP, UDP, or SCTP) which
traffic must match. If not specified, this field
defaults to TCP.
type: string
type: object
type: array
to:
description: List of destinations for outgoing traffic of
pods selected for this rule. Items in this list are combined
using a logical OR operation. If this field is empty or
missing, this rule matches all destinations (traffic not
restricted by destination). If this field is present and
contains at least one item, this rule allows traffic only
if the traffic matches at least one item in the to list.
items:
description: NetworkPolicyPeer describes a peer to allow
traffic from. Only certain combinations of fields are
allowed
properties:
ipBlock:
description: IPBlock defines policy on a particular
IPBlock. If this field is set then neither of the
other fields can be.
properties:
cidr:
description: CIDR is a string representing the
IP Block Valid examples are "192.168.1.1/24"
or "2001:db9::/64"
type: string
except:
description: Except is a slice of CIDRs that should
not be included within an IP Block Valid examples
are "192.168.1.1/24" or "2001:db9::/64" Except
values will be rejected if they are outside
the CIDR range
items:
type: string
type: array
required:
- cidr
type: object
namespaceSelector:
description: "Selects Namespaces using cluster-scoped
labels. This field follows standard label selector
semantics; if present but empty, it selects all
namespaces. \n If PodSelector is also set, then
the NetworkPolicyPeer as a whole selects the Pods
matching PodSelector in the Namespaces selected
by NamespaceSelector. Otherwise it selects all Pods
in the Namespaces selected by NamespaceSelector."
properties:
matchExpressions:
description: matchExpressions is a list of label
selector requirements. The requirements are
ANDed.
items:
description: A label selector requirement is
a selector that contains values, a key, and
an operator that relates the key and values.
properties:
key:
description: key is the label key that the
selector applies to.
type: string
operator:
description: operator represents a key's
relationship to a set of values. Valid
operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: values is an array of string
values. If the operator is In or NotIn,
the values array must be non-empty. If
the operator is Exists or DoesNotExist,
the values array must be empty. This array
is replaced during a strategic merge patch.
items:
type: string
type: array
required:
- key
- operator
type: object
type: array
matchLabels:
additionalProperties:
type: string
description: matchLabels is a map of {key,value}
pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions,
whose key field is "key", the operator is "In",
and the values array contains only "value".
The requirements are ANDed.
type: object
type: object
podSelector:
description: "This is a label selector which selects
Pods. This field follows standard label selector
semantics; if present but empty, it selects all
pods. \n If NamespaceSelector is also set, then
the NetworkPolicyPeer as a whole selects the Pods
matching PodSelector in the Namespaces selected
by NamespaceSelector. Otherwise it selects the Pods
matching PodSelector in the policy's own Namespace."
properties:
matchExpressions:
description: matchExpressions is a list of label
selector requirements. The requirements are
ANDed.
items:
description: A label selector requirement is
a selector that contains values, a key, and
an operator that relates the key and values.
properties:
key:
description: key is the label key that the
selector applies to.
type: string
operator:
description: operator represents a key's
relationship to a set of values. Valid
operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: values is an array of string
values. If the operator is In or NotIn,
the values array must be non-empty. If
the operator is Exists or DoesNotExist,
the values array must be empty. This array
is replaced during a strategic merge patch.
items:
type: string
type: array
required:
- key
- operator
type: object
type: array
matchLabels:
additionalProperties:
type: string
description: matchLabels is a map of {key,value}
pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions,
whose key field is "key", the operator is "In",
and the values array contains only "value".
The requirements are ANDed.
type: object
type: object
type: object
type: array
type: object
type: array
ingress:
description: List of ingress rules to be applied to the selected
pods. Traffic is allowed to a pod if there are no NetworkPolicies
selecting the pod (and cluster policy otherwise allows the traffic),
OR if the traffic source is the pod's local node, OR if the
traffic matches at least one ingress rule across all of the
NetworkPolicy objects whose podSelector matches the pod. If
this field is empty then this NetworkPolicy does not allow any
traffic (and serves solely to ensure that the pods it selects
are isolated by default)
items:
description: NetworkPolicyIngressRule describes a particular
set of traffic that is allowed to the pods matched by a NetworkPolicySpec's
podSelector. The traffic must match both ports and from.
properties:
from:
description: List of sources which should be able to access
the pods selected for this rule. Items in this list are
combined using a logical OR operation. If this field is
empty or missing, this rule matches all sources (traffic
not restricted by source). If this field is present and
contains at least one item, this rule allows traffic only
if the traffic matches at least one item in the from list.
items:
description: NetworkPolicyPeer describes a peer to allow
traffic from. Only certain combinations of fields are
allowed
properties:
ipBlock:
description: IPBlock defines policy on a particular
IPBlock. If this field is set then neither of the
other fields can be.
properties:
cidr:
description: CIDR is a string representing the
IP Block Valid examples are "192.168.1.1/24"
or "2001:db9::/64"
type: string
except:
description: Except is a slice of CIDRs that should
not be included within an IP Block Valid examples
are "192.168.1.1/24" or "2001:db9::/64" Except
values will be rejected if they are outside
the CIDR range
items:
type: string
type: array
required:
- cidr
type: object
namespaceSelector:
description: "Selects Namespaces using cluster-scoped
labels. This field follows standard label selector
semantics; if present but empty, it selects all
namespaces. \n If PodSelector is also set, then
the NetworkPolicyPeer as a whole selects the Pods
matching PodSelector in the Namespaces selected
by NamespaceSelector. Otherwise it selects all Pods
in the Namespaces selected by NamespaceSelector."
properties:
matchExpressions:
description: matchExpressions is a list of label
selector requirements. The requirements are
ANDed.
items:
description: A label selector requirement is
a selector that contains values, a key, and
an operator that relates the key and values.
properties:
key:
description: key is the label key that the
selector applies to.
type: string
operator:
description: operator represents a key's
relationship to a set of values. Valid
operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: values is an array of string
values. If the operator is In or NotIn,
the values array must be non-empty. If
the operator is Exists or DoesNotExist,
the values array must be empty. This array
is replaced during a strategic merge patch.
items:
type: string
type: array
required:
- key
- operator
type: object
type: array
matchLabels:
additionalProperties:
type: string
description: matchLabels is a map of {key,value}
pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions,
whose key field is "key", the operator is "In",
and the values array contains only "value".
The requirements are ANDed.
type: object
type: object
podSelector:
description: "This is a label selector which selects
Pods. This field follows standard label selector
semantics; if present but empty, it selects all
pods. \n If NamespaceSelector is also set, then
the NetworkPolicyPeer as a whole selects the Pods
matching PodSelector in the Namespaces selected
by NamespaceSelector. Otherwise it selects the Pods
matching PodSelector in the policy's own Namespace."
properties:
matchExpressions:
description: matchExpressions is a list of label
selector requirements. The requirements are
ANDed.
items:
description: A label selector requirement is
a selector that contains values, a key, and
an operator that relates the key and values.
properties:
key:
description: key is the label key that the
selector applies to.
type: string
operator:
description: operator represents a key's
relationship to a set of values. Valid
operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: values is an array of string
values. If the operator is In or NotIn,
the values array must be non-empty. If
the operator is Exists or DoesNotExist,
the values array must be empty. This array
is replaced during a strategic merge patch.
items:
type: string
type: array
required:
- key
- operator
type: object
type: array
matchLabels:
additionalProperties:
type: string
description: matchLabels is a map of {key,value}
pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions,
whose key field is "key", the operator is "In",
and the values array contains only "value".
The requirements are ANDed.
type: object
type: object
type: object
type: array
ports:
description: List of ports which should be made accessible
on the pods selected for this rule. Each item in this
list is combined using a logical OR. If this field is
empty or missing, this rule matches all ports (traffic
not restricted by port). If this field is present and
contains at least one item, then this rule allows traffic
only if the traffic matches at least one port in the list.
items:
description: NetworkPolicyPort describes a port to allow
traffic on
properties:
port:
anyOf:
- type: integer
- type: string
description: The port on the given protocol. This
can either be a numerical or named port on a pod.
If this field is not provided, this matches all
port names and numbers.
x-kubernetes-int-or-string: true
protocol:
description: The protocol (TCP, UDP, or SCTP) which
traffic must match. If not specified, this field
defaults to TCP.
type: string
type: object
type: array
type: object
type: array
podSelector:
description: Selects the pods to which this NetworkPolicy object
applies. The array of ingress rules is applied to any pods selected
by this field. Multiple network policies can select the same
set of pods. In this case, the ingress rules for each are combined
additively. This field is NOT optional and follows standard
label selector semantics. An empty podSelector matches all pods
in this namespace.
properties:
matchExpressions:
description: matchExpressions is a list of label selector
requirements. The requirements are ANDed.
items:
description: A label selector requirement is a selector
that contains values, a key, and an operator that relates
the key and values.
properties:
key:
description: key is the label key that the selector
applies to.
type: string
operator:
description: operator represents a key's relationship
to a set of values. Valid operators are In, NotIn,
Exists and DoesNotExist.
type: string
values:
description: values is an array of string values. If
the operator is In or NotIn, the values array must
be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced
during a strategic merge patch.
items:
type: string
type: array
required:
- key
- operator
type: object
type: array
matchLabels:
additionalProperties:
type: string
description: matchLabels is a map of {key,value} pairs. A
single {key,value} in the matchLabels map is equivalent
to an element of matchExpressions, whose key field is "key",
the operator is "In", and the values array contains only
"value". The requirements are ANDed.
type: object
type: object
policyTypes:
description: List of rule types that the NetworkPolicy relates
to. Valid options are "Ingress", "Egress", or "Ingress,Egress".
If this field is not specified, it will default based on the
existence of Ingress or Egress rules; policies that contain
an Egress section are assumed to affect Egress, and all policies
(whether or not they contain an Ingress section) are assumed
to affect Ingress. If you want to write an egress-only policy,
you must explicitly specify policyTypes [ "Egress" ]. Likewise,
if you want to write a policy that specifies that no egress
is allowed, you must specify a policyTypes value that include
"Egress" (since such a policy would not include an Egress section
and would otherwise default to just [ "Ingress" ]). This field
is beta-level in 1.8
items:
description: Policy Type string describes the NetworkPolicy
type This type is beta-level in 1.8
type: string
type: array
required:
- podSelector
type: object
type: array
nodeSelector:
additionalProperties:
type: string
type: object
owner:
type: string
resourceQuotas:
items:
description: ResourceQuotaSpec defines the desired hard limits to
enforce for Quota.
properties:
hard:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: 'hard is the set of desired hard limits for each
named resource. More info: https://kubernetes.io/docs/concepts/policy/resource-quotas/'
type: object
scopeSelector:
description: scopeSelector is also a collection of filters like
scopes that must match each object tracked by a quota but expressed
using ScopeSelectorOperator in combination with possible values.
For a resource to match, both scopes AND scopeSelector (if specified
in spec), must be matched.
properties:
matchExpressions:
description: A list of scope selector requirements by scope
of the resources.
items:
description: A scoped-resource selector requirement is a
selector that contains values, a scope name, and an operator
that relates the scope name and values.
properties:
operator:
description: Represents a scope's relationship to a
set of values. Valid operators are In, NotIn, Exists,
DoesNotExist.
type: string
scopeName:
description: The name of the scope that the selector
applies to.
type: string
values:
description: An array of string values. If the operator
is In or NotIn, the values array must be non-empty.
If the operator is Exists or DoesNotExist, the values
array must be empty. This array is replaced during
a strategic merge patch.
items:
type: string
type: array
required:
- operator
- scopeName
type: object
type: array
type: object
scopes:
description: A collection of filters that must match each object
tracked by a quota. If not specified, the quota matches all
objects.
items:
description: A ResourceQuotaScope defines a filter that must
match each object tracked by a quota
type: string
type: array
type: object
type: array
storageClasses:
items:
type: string
type: array
required:
- ingressClasses
- limitRanges
- namespaceQuota
- owner
- storageClasses
type: object
status:
description: TenantStatus defines the observed state of Tenant
properties:
groups:
items:
type: string
type: array
namespaces:
items:
type: string
type: array
size:
type: integer
users:
items:
type: string
type: array
required:
- size
type: object
type: object
version: v1alpha1
versions:
- name: v1alpha1
served: true
storage: true
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@@ -0,0 +1,10 @@
# This kustomization.yaml is not intended to be run by itself,
# since it depends on service name and namespace that are out of this kustomize package.
# It should be run by config/default
resources:
- bases/capsule.clastix.io_tenants.yaml
# +kubebuilder:scaffold:crdkustomizeresource
# the following config is for teaching kustomize how to do kustomization for CRDs.
configurations:
- kustomizeconfig.yaml

View File

@@ -0,0 +1,17 @@
# This file is for teaching kustomize how to substitute name and namespace reference in CRD
nameReference:
- kind: Service
version: v1
fieldSpecs:
- kind: CustomResourceDefinition
group: apiextensions.k8s.io
path: spec/conversion/webhookClientConfig/service/name
namespace:
- kind: CustomResourceDefinition
group: apiextensions.k8s.io
path: spec/conversion/webhookClientConfig/service/namespace
create: false
varReference:
- path: metadata/annotations

View File

@@ -0,0 +1,30 @@
# Adds namespace to all resources.
namespace: capsule-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: capsule-
# Labels to add to all resources and selectors.
#commonLabels:
# someName: someValue
bases:
- ../crd
- ../rbac
- ../manager
- ../secret
- ../webhook
- ../tenants
# [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
- manager_webhook_patch.yaml

View File

@@ -0,0 +1,25 @@
# 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.5.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:
- "--metrics-addr=127.0.0.1:8080"
- "--enable-leader-election"

View File

@@ -0,0 +1,23 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
namespace: system
spec:
template:
spec:
containers:
- name: manager
ports:
- containerPort: 9443
name: webhook-server
protocol: TCP
volumeMounts:
- mountPath: /tmp/k8s-webhook-server/serving-certs
name: cert
readOnly: true
volumes:
- name: cert
secret:
defaultMode: 420
secretName: capsule-tls

View File

@@ -0,0 +1,8 @@
resources:
- manager.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
images:
- name: controller
newName: quay.io/clastix/capsule
newTag: latest

View File

@@ -0,0 +1,40 @@
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:
labels:
control-plane: controller-manager
spec:
containers:
- command:
- /manager
args:
- --enable-leader-election
image: quay.io/clastix/capsule:latest
imagePullPolicy: IfNotPresent
name: manager
resources:
limits:
cpu: 200m
memory: 128Mi
requests:
cpu: 200m
memory: 128Mi
terminationGracePeriodSeconds: 10

View File

@@ -0,0 +1,2 @@
resources:
- monitor.yaml

View File

@@ -0,0 +1,16 @@
# 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
selector:
matchLabels:
control-plane: controller-manager

View File

@@ -0,0 +1,7 @@
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: metrics-reader
rules:
- nonResourceURLs: ["/metrics"]
verbs: ["get"]

View File

@@ -0,0 +1,13 @@
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"]

View File

@@ -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: default
namespace: system

View File

@@ -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

View File

@@ -0,0 +1,9 @@
resources:
- 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

View File

@@ -0,0 +1,12 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: manager-rolebinding
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io
subjects:
- kind: ServiceAccount
name: default
namespace: system

View File

@@ -0,0 +1,3 @@
## This file is auto-generated, do not modify ##
resources:
- capsule_v1alpha1_tenant.yaml

View File

@@ -0,0 +1,3 @@
resources:
- secret-ca.yaml
- secret-tls.yaml

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Secret
metadata:
name: ca

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Secret
metadata:
name: tls

View File

@@ -0,0 +1,3 @@
resources:
- namespace-deleter.yaml
- namespace-provisioner.yaml

View File

@@ -0,0 +1,6 @@
resources:
- manifests.yaml
- service.yaml
configurations:
- kustomizeconfig.yaml

View File

@@ -0,0 +1,25 @@
# the following config is for teaching kustomize where to look at when substituting vars.
# It requires kustomize v2.1.0 or newer to work properly.
nameReference:
- kind: Service
version: v1
fieldSpecs:
- kind: MutatingWebhookConfiguration
group: admissionregistration.k8s.io
path: webhooks/clientConfig/service/name
- kind: ValidatingWebhookConfiguration
group: admissionregistration.k8s.io
path: webhooks/clientConfig/service/name
namespace:
- kind: MutatingWebhookConfiguration
group: admissionregistration.k8s.io
path: webhooks/clientConfig/service/namespace
create: true
- kind: ValidatingWebhookConfiguration
group: admissionregistration.k8s.io
path: webhooks/clientConfig/service/namespace
create: true
varReference:
- path: metadata/annotations

View File

@@ -0,0 +1,122 @@
---
apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
creationTimestamp: null
name: mutating-webhook-configuration
webhooks:
- clientConfig:
caBundle: Cg==
service:
name: webhook-service
namespace: system
path: /mutate-v1-namespace-owner-reference
failurePolicy: Fail
name: owner.namespace.capsule.clastix.io
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- namespaces
---
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
creationTimestamp: null
name: validating-webhook-configuration
webhooks:
- clientConfig:
caBundle: Cg==
service:
name: webhook-service
namespace: system
path: /validating-v1-extensions-ingress
failurePolicy: Fail
name: extensions.ingress.capsule.clastix.io
rules:
- apiGroups:
- extensions
apiVersions:
- v1beta1
operations:
- CREATE
- UPDATE
resources:
- ingresses
- clientConfig:
caBundle: Cg==
service:
name: webhook-service
namespace: system
path: /validating-v1-networking-ingress
failurePolicy: Fail
name: networking.ingress.capsule.clastix.io
rules:
- apiGroups:
- networking.k8s.io
apiVersions:
- v1beta1
operations:
- CREATE
- UPDATE
resources:
- ingresses
- clientConfig:
caBundle: Cg==
service:
name: webhook-service
namespace: system
path: /validate-v1-namespace-quota
failurePolicy: Fail
name: quota.namespace.capsule.clastix.io
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- namespaces
- clientConfig:
caBundle: Cg==
service:
name: webhook-service
namespace: system
path: /validating-v1-network-policy
failurePolicy: Fail
name: validating.network-policy.capsule.clastix.io
rules:
- apiGroups:
- networking.k8s.io
apiVersions:
- v1
operations:
- CREATE
- UPDATE
- DELETE
resources:
- networkpolicies
- clientConfig:
caBundle: Cg==
service:
name: webhook-service
namespace: system
path: /validating-v1-pvc
failurePolicy: Fail
name: pvc.capsule.clastix.io
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- persistentvolumeclaims

View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: webhook-service
spec:
ports:
- port: 443
targetPort: 9443
selector:
control-plane: controller-manager

View File

@@ -9,7 +9,8 @@ The first step is to setup your local development environment
The following dependencies are mandatory:
- [Go 1.13.8](https://golang.org/dl/)
- [OperatorSDK 1.8](https://github.com/operator-framework/operator-sdk)
- [OperatorSDK 1.9](https://github.com/operator-framework/operator-sdk)
- [Kubebuilder](https://github.com/kubernetes-sigs/kubebuilder)
- [KinD](https://github.com/kubernetes-sigs/kind)
- [ngrok](https://ngrok.com/) (if you want to run locally)
- [golangci-lint](https://github.com/golangci/golangci-lint)
@@ -29,6 +30,13 @@ Some operations, like the Docker image build process or the code-generation of
the CRDs manifests, as well the deep copy functions, require _Operator SDK_:
the binary has to be installed into your `PATH`.
### Installing Kubebuilder
With the latest release of OperatorSDK there's a more tightly integration with
Kubebuilder and its opinionated testing suite: ensure to download the latest
binaries available from the _Releases_ GitHub page and place them into the
`/usr/local/kubebuilder/bin` folder, ensuring this is also in your `PATH`.
### Installing KinD
Capsule is able to run on any certified Kubernetes installation and locally
@@ -68,37 +76,63 @@ certificates and the context changed to the just born Kubernetes cluster.
From the root path, issue the _make_ recipe:
```
# make docker-image
operator-sdk build quay.io/clastix/capsule:latest
INFO[0001] Building OCI image quay.io/clastix/capsule:latest
Sending build context to Docker daemon 89.26MB
Step 1/7 : FROM registry.access.redhat.com/ubi8/ubi-minimal:latest
---> 75a64ccf990b
Step 2/7 : ENV OPERATOR=/usr/local/bin/capsule USER_UID=0 USER_NAME=capsule
# make docker-build
/home/prometherion/go/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
main.go
go vet ./...
/home/prometherion/go/bin/controller-gen "crd:trivialVersions=true" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
go test ./... -coverprofile cover.out
...
docker build . -t quay.io/clastix/capsule:latest
Sending build context to Docker daemon 43.21MB
Step 1/15 : FROM golang:1.13 as builder
---> 67d10cb69049
Step 2/15 : WORKDIR /workspace
---> Using cache
---> e4610bd8596f
Step 3/7 : COPY build/_output/bin/capsule ${OPERATOR}
---> d783cc2b7c33
Step 3/15 : COPY go.mod go.mod
---> Using cache
---> 1f6196485c28
Step 4/7 : COPY build/bin /usr/local/bin
---> 0fec3ca39e50
Step 4/15 : COPY go.sum go.sum
---> Using cache
---> b517a62ca352
Step 5/7 : RUN /usr/local/bin/user_setup
---> de15be20dbe7
Step 5/15 : RUN go mod download
---> Using cache
---> e879394010d5
Step 6/7 : ENTRYPOINT ["/usr/local/bin/entrypoint"]
---> b525cd9abc67
Step 6/15 : COPY main.go main.go
---> 67d9d6538ffc
Step 7/15 : COPY api/ api/
---> 6243b250d170
Step 8/15 : COPY controllers/ controllers/
---> 4abf8ce85484
Step 9/15 : COPY pkg/ pkg/
---> 2cd289b1d496
Step 10/15 : RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o manager main.go
---> Running in dac9a1e3b23f
Removing intermediate container dac9a1e3b23f
---> bb650a8efcb2
Step 11/15 : FROM gcr.io/distroless/static:nonroot
---> 131713291b92
Step 12/15 : WORKDIR /
---> Using cache
---> 6e740290e0e4
Step 7/7 : USER ${USER_UID}
---> Using cache
---> ebb8f640dda1
Successfully built ebb8f640dda1
---> 677a73ab94d3
Step 13/15 : COPY --from=builder /workspace/manager .
---> 6ecb58a82c0a
Step 14/15 : USER nonroot:nonroot
---> Running in a0b8c95f85d4
Removing intermediate container a0b8c95f85d4
---> c4897d60a094
Step 15/15 : ENTRYPOINT ["/manager"]
---> Running in 1a42bab52aa7
Removing intermediate container 1a42bab52aa7
---> 37d2adbe2669
Successfully built 37d2adbe2669
Successfully tagged quay.io/clastix/capsule:latest
INFO[0004] Operator build complete.
```
The image `quay.io/clastix/capsule:latest` will be available locally, you just
need to push it to kind with the following command.
need to push it to _KinD_ with the following command.
```
# kind load docker-image --nodes capsule-control-plane --name capsule quay.io/clastix/capsule:latest
@@ -107,45 +141,34 @@ Image: "quay.io/clastix/capsule:latest" with ID "sha256:ebb8f640dda129a795ddc68b
### Deploy the Kubernetes manifests
With the current `kind-capsule` context enabled, create the `capsule-system`
Namespace that will contain all the Kubernetes resources.
With the current `kind-capsule` context enabled, deploy all the required
manifests issuing the following command:
```
# kubectl create namespace capsule-system
namespace/capsule-system created
make deploy
```
Now it's time to install the _Custom Resource Definition_:
This will install all the required Kubernetes resources, automatically.
You can check if Capsule is running tailing the logs:
```
# kubectl apply -f deploy/crds/capsule.clastix.io_tenants_crd.yaml
customresourcedefinition.apiextensions.k8s.io/tenants.capsule.clastix.io created
```
Finally, install the required manifests issuing the following command:
```
# kubectl apply -f deploy
mutatingwebhookconfiguration.admissionregistration.k8s.io/capsule created
clusterrole.rbac.authorization.k8s.io/namespace:deleter created
clusterrole.rbac.authorization.k8s.io/namespace:provisioner created
clusterrolebinding.rbac.authorization.k8s.io/namespace:provisioner created
deployment.apps/capsule created
clusterrole.rbac.authorization.k8s.io/capsule created
clusterrolebinding.rbac.authorization.k8s.io/capsule-cluster-admin created
clusterrolebinding.rbac.authorization.k8s.io/capsule created
secret/capsule-ca created
secret/capsule-tls created
service/capsule created
serviceaccount/capsule created
```
You can check if Capsule is running checking the logs:
```
# kubectl -n capsule-system logs -f -l name=capsule
# kubectl -n capsule-system logs --all-containers -f -l control-plane=controller-manager
...
{"level":"info","ts":1596125071.5951712,"logger":"controller-runtime.controller","msg":"Starting workers","controller":"tenant-controller","worker count":1}
2020-08-03T15:37:44.031Z INFO controllers.Tenant Role Binding sync result: unchanged {"Request.Name": "oil", "name": "namespace:deleter", "namespace": "oil-dev"}
2020-08-03T15:37:44.032Z INFO controllers.Tenant Role Binding sync result: unchanged {"Request.Name": "oil", "name": "namespace:admin", "namespace": "oil-production"}
2020-08-03T15:37:44.032Z INFO controllers.Tenant Role Binding sync result: unchanged {"Request.Name": "oil", "name": "namespace:deleter", "namespace": "oil-production"}
2020-08-03T15:37:44.032Z INFO controllers.Tenant Tenant reconciling completed {"Request.Name": "oil"}
2020-08-03T15:37:44.032Z DEBUG controller-runtime.controller Successfully Reconciled {"controller": "tenant", "request": "/oil"}
2020-08-03T15:37:46.945Z INFO controllers.Namespace Reconciling Namespace {"Request.Name": "oil-staging"}
2020-08-03T15:37:46.953Z INFO controllers.Namespace Namespace reconciliation processed {"Request.Name": "oil-staging"}
2020-08-03T15:37:46.953Z DEBUG controller-runtime.controller Successfully Reconciled {"controller": "namespace", "request": "/oil-staging"}
2020-08-03T15:37:46.957Z INFO controllers.Namespace Reconciling Namespace {"Request.Name": "oil-staging"}
2020-08-03T15:37:46.957Z DEBUG controller-runtime.controller Successfully Reconciled {"controller": "namespace", "request": "/oil-staging"}
I0803 15:16:01.763606 1 main.go:186] Valid token audiences:
I0803 15:16:01.763689 1 main.go:232] Generating self signed cert as no cert is provided
I0803 15:16:02.042022 1 main.go:281] Starting TCP socket on 0.0.0.0:8443
I0803 15:16:02.042364 1 main.go:288] Listening securely on 0.0.0.0:8443
```
Since Capsule is built using _OperatorSDK_, logging is handled by the zap
@@ -169,8 +192,8 @@ access to the Kubernetes API Server.
First, ensure the Capsule pod is not running scaling down the Deployment.
```
# kubectl -n capsule-system scale deployment capsule --replicas=0
deployment.apps/capsule scaled
# kubectl -n capsule-system scale deployment capsule-controller-manager --replicas=0
deployment.apps/capsule-controller-manager scaled
```
> This is mandatory since Capsule uses Leader Election
@@ -197,7 +220,7 @@ In another session we need a `ngrok` session, mandatory to debug also webhooks
(YMMV).
```
# ngrok http localhost:443
# ngrok http https://localhost:9443
ngrok by @inconshreveable
Session Status online
@@ -205,8 +228,8 @@ Account Dario Tranchitella (Plan: Free)
Version 2.3.35
Region United States (us)
Web Interface http://127.0.01:4040
Forwarding http://cdb72b99348c.ngrok.io -> https://localhost:443
Forwarding https://cdb72b99348c.ngrok.io -> https://localhost:443
Forwarding http://cdb72b99348c.ngrok.io -> https://localhost:9443
Forwarding https://cdb72b99348c.ngrok.io -> https://localhost:9443
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
```
@@ -217,14 +240,15 @@ _Dynamic Admissions Control Webhooks_.
#### Patching the MutatingWebhookConfiguration
Now it's time to patch the _MutatingWebhookConfiguration_, adding the said
`ngrok` URL as base for each defined webhook, as following:
Now it's time to patch the _MutatingWebhookConfiguration_ and the
_ValidatingWebhookConfiguration_ too, adding the said `ngrok` URL as base for
each defined webhook, as following:
```diff
apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
name: capsule
name: capsule-mutating-webhook-configuration
webhooks:
- name: owner.namespace.capsule.clastix.io
failurePolicy: Fail
@@ -237,7 +261,7 @@ webhooks:
+ url: https://cdb72b99348c.ngrok.io/mutate-v1-namespace-owner-reference
- caBundle:
- service:
- namespace: capsule-system
- namespace: system
- name: capsule
- path: /mutate-v1-namespace-owner-reference
...
@@ -249,7 +273,7 @@ Finally, it's time to run locally Capsule using your preferred IDE (or not):
from the project root path you can issue the following command.
```
WATCH_NAMESPACE= KUBECONFIG=/path/to/your/kubeconfig OPERATOR_NAME=capsule go run cmd/manager/main.go
make run
```
All the logs will start to flow in your standard output, feel free to attach

View File

@@ -1,9 +1,12 @@
/*
Copyright 2020 Clastix Labs.
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.
@@ -11,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package namespace
package controllers
import (
"context"
@@ -24,106 +27,81 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
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/controller"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
"github.com/clastix/capsule/pkg/apis/capsule/v1alpha1"
"github.com/clastix/capsule/api/v1alpha1"
)
// Add creates a new Namespace Controller and adds it to the Manager. The Manager will set fields on the Controller
// and Start it when the Manager is Started.
func Add(mgr manager.Manager) error {
return add(mgr, newReconciler(mgr))
type NamespaceReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
}
// newReconciler returns a new reconcile.Reconciler
func newReconciler(mgr manager.Manager) reconcile.Reconciler {
return &ReconcileNamespace{
client: mgr.GetClient(),
scheme: mgr.GetScheme(),
}
func (r *NamespaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&corev1.Namespace{}, builder.WithPredicates(predicate.Funcs{
CreateFunc: func(event event.CreateEvent) (ok bool) {
ok, _ = getCapsuleReference(event.Meta.GetOwnerReferences())
return
},
DeleteFunc: func(deleteEvent event.DeleteEvent) (ok bool) {
ok, _ = getCapsuleReference(deleteEvent.Meta.GetOwnerReferences())
return
},
UpdateFunc: func(updateEvent event.UpdateEvent) (ok bool) {
ok, _ = getCapsuleReference(updateEvent.MetaNew.GetOwnerReferences())
return
},
GenericFunc: func(genericEvent event.GenericEvent) (ok bool) {
ok, _ = getCapsuleReference(genericEvent.Meta.GetOwnerReferences())
return
},
})).
Complete(r)
}
func getCapsuleReference(refs []v1.OwnerReference) (ok bool, reference *v1.OwnerReference) {
for _, r := range refs {
if r.APIVersion == v1alpha1.SchemeGroupVersion.String() {
if r.APIVersion == v1alpha1.GroupVersion.String() {
return true, r.DeepCopy()
}
}
return false, nil
}
// add adds a new Controller to mgr with r as the reconcile.Reconciler
func add(mgr manager.Manager, r reconcile.Reconciler) error {
// Create a new controller
c, err := controller.New("namespace-controller", mgr, controller.Options{Reconciler: r})
if err != nil {
return err
}
// Watch for changes to primary resource Namespace
err = c.Watch(&source.Kind{Type: &corev1.Namespace{}}, &handler.EnqueueRequestForObject{}, predicate.Funcs{
CreateFunc: func(event event.CreateEvent) (ok bool) {
ok, _ = getCapsuleReference(event.Meta.GetOwnerReferences())
return
},
DeleteFunc: func(deleteEvent event.DeleteEvent) (ok bool) {
ok, _ = getCapsuleReference(deleteEvent.Meta.GetOwnerReferences())
return
},
UpdateFunc: func(updateEvent event.UpdateEvent) (ok bool) {
ok, _ = getCapsuleReference(updateEvent.MetaNew.GetOwnerReferences())
return
},
GenericFunc: func(genericEvent event.GenericEvent) (ok bool) {
ok, _ = getCapsuleReference(genericEvent.Meta.GetOwnerReferences())
return
},
})
if err != nil {
return err
}
return nil
}
// ReconcileNamespace reconciles a Namespace object
type ReconcileNamespace struct {
client client.Client
scheme *runtime.Scheme
logger logr.Logger
}
func (r *ReconcileNamespace) removeNamespace(name string, tenant *v1alpha1.Tenant) {
func (r *NamespaceReconciler) removeNamespace(name string, tenant *v1alpha1.Tenant) {
c := tenant.Status.Namespaces.DeepCopy()
sort.Sort(c)
i := sort.SearchStrings(c, name)
// namespace already removed, do nothing
if i > c.Len() || i == c.Len() {
r.Log.Info("Namespace has been already removed")
return
}
// namespace is there, removing it
r.Log.Info("Removing Namespace from Tenant status")
tenant.Status.Namespaces = []string{}
tenant.Status.Namespaces = append(tenant.Status.Namespaces, c[:i]...)
tenant.Status.Namespaces = append(tenant.Status.Namespaces, c[i+1:]...)
}
func (r *ReconcileNamespace) addNamespace(name string, tenant *v1alpha1.Tenant) {
func (r *NamespaceReconciler) addNamespace(name string, tenant *v1alpha1.Tenant) {
c := tenant.Status.Namespaces.DeepCopy()
sort.Sort(c)
i := sort.SearchStrings(c, name)
// namespace already there, nothing to do
if i < c.Len() && c[i] == name {
r.Log.Info("Namespace has been already added")
return
}
// missing namespace, let's append it
r.Log.Info("Adding Namespace to Tenant status")
if i == 0 {
tenant.Status.Namespaces = []string{name}
} else {
@@ -134,21 +112,20 @@ func (r *ReconcileNamespace) addNamespace(name string, tenant *v1alpha1.Tenant)
tenant.Status.Namespaces = append(tenant.Status.Namespaces, c[i:]...)
}
func (r *ReconcileNamespace) updateNamespaceCount(tenant *v1alpha1.Tenant) error {
tenant.Status.Size = uint(len(tenant.Status.Namespaces))
func (r *NamespaceReconciler) updateNamespaceCount(tenant *v1alpha1.Tenant) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
return r.client.Status().Update(context.TODO(), tenant, &client.UpdateOptions{})
tenant.Status.Size = uint(len(tenant.Status.Namespaces))
return r.Client.Status().Update(context.TODO(), tenant, &client.UpdateOptions{})
})
}
func (r *ReconcileNamespace) Reconcile(request reconcile.Request) (res reconcile.Result, err error) {
r.logger = log.Log.WithName("controller_namespace").WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
r.logger.Info("Reconciling Namespace")
func (r NamespaceReconciler) Reconcile(request ctrl.Request) (ctrl.Result, error) {
r.Log = r.Log.WithValues("Request.Name", request.Name)
r.Log.Info("Reconciling Namespace")
// Fetch the Namespace instance
ns := &corev1.Namespace{}
if err := r.client.Get(context.TODO(), request.NamespacedName, ns); err != nil {
if err := r.Get(context.TODO(), request.NamespacedName, ns); err != nil {
if errors.IsNotFound(err) {
// Request object not found, could have been deleted after reconcile request.
// Owned objects are automatically garbage collected. For additional cleanup logic use finalizers.
@@ -161,27 +138,27 @@ func (r *ReconcileNamespace) Reconcile(request reconcile.Request) (res reconcile
_, or := getCapsuleReference(ns.OwnerReferences)
t := &v1alpha1.Tenant{}
if err := r.client.Get(context.TODO(), types.NamespacedName{Name: or.Name}, t); err != nil {
if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: or.Name}, t); err != nil {
// Error reading the object - requeue the request.
return reconcile.Result{}, err
}
if err := r.ensureLabel(ns, t.Name); err != nil {
r.logger.Error(err, "cannot update Namespace label")
r.Log.Error(err, "cannot update Namespace label")
return reconcile.Result{}, err
}
r.updateTenantStatus(ns, t)
if err := r.updateNamespaceCount(t); err != nil {
r.logger.Error(err, "cannot update Namespace list", "tenant", t.Name)
r.Log.Error(err, "cannot update Namespace list", "tenant", t.Name)
}
r.logger.Info("Namespace reconciliation processed")
r.Log.Info("Namespace reconciliation processed")
return reconcile.Result{}, nil
}
func (r *ReconcileNamespace) ensureLabel(ns *corev1.Namespace, tenantName string) error {
func (r *NamespaceReconciler) ensureLabel(ns *corev1.Namespace, tenantName string) error {
capsuleLabel, err := v1alpha1.GetTypeLabel(&v1alpha1.Tenant{})
if err != nil {
return err
@@ -191,15 +168,15 @@ func (r *ReconcileNamespace) ensureLabel(ns *corev1.Namespace, tenantName string
}
tl, ok := ns.Labels[capsuleLabel]
if !ok || tl != tenantName {
ns.Labels[capsuleLabel] = tenantName
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
return r.client.Update(context.TODO(), ns, &client.UpdateOptions{})
ns.Labels[capsuleLabel] = tenantName
return r.Client.Update(context.TODO(), ns, &client.UpdateOptions{})
})
}
return nil
}
func (r *ReconcileNamespace) updateTenantStatus(ns *corev1.Namespace, tenant *v1alpha1.Tenant) {
func (r *NamespaceReconciler) updateTenantStatus(ns *corev1.Namespace, tenant *v1alpha1.Tenant) {
switch ns.Status.Phase {
case corev1.NamespaceTerminating:
r.removeNamespace(ns.Name, tenant)

177
controllers/secret/ca.go Normal file
View File

@@ -0,0 +1,177 @@
/*
Copyright 2020 Clastix Labs.
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 secret
import (
"bytes"
"context"
"errors"
"time"
"github.com/go-logr/logr"
v1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/clastix/capsule/pkg/cert"
)
type CaReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
}
func (r *CaReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&corev1.Secret{}, forOptionPerInstanceName(caSecretName)).
Complete(r)
}
func (r CaReconciler) Reconcile(request ctrl.Request) (ctrl.Result, error) {
var err error
r.Log = log.Log.WithName("controller_ca").WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
r.Log.Info("Reconciling CA Secret")
// Fetch the CA instance
instance := &corev1.Secret{}
err = r.Client.Get(context.TODO(), request.NamespacedName, instance)
if err != nil {
// Error reading the object - requeue the request.
return reconcile.Result{}, err
}
var ca cert.Ca
var rq time.Duration
ca, err = getCertificateAuthority(r.Client)
if err != nil && errors.Is(err, MissingCaError{}) {
ca, err = cert.GenerateCertificateAuthority()
if err != nil {
return reconcile.Result{}, err
}
} else if err != nil {
return reconcile.Result{}, err
}
r.Log.Info("Handling CA Secret")
rq, err = ca.ExpiresIn(time.Now())
if err != nil {
r.Log.Info("CA is expired, cleaning to obtain a new one")
instance.Data = map[string][]byte{}
} else {
r.Log.Info("Updating CA secret with new PEM and RSA")
var crt *bytes.Buffer
var key *bytes.Buffer
crt, _ = ca.CaCertificatePem()
key, _ = ca.CaPrivateKeyPem()
instance.Data = map[string][]byte{
certSecretKey: crt.Bytes(),
privateKeySecretKey: key.Bytes(),
}
// Updating ValidatingWebhookConfiguration CA bundle
err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
vw := &v1.ValidatingWebhookConfiguration{}
err = r.Get(context.TODO(), types.NamespacedName{Name: "capsule-validating-webhook-configuration"}, vw)
if err != nil {
r.Log.Error(err, "cannot retrieve ValidatingWebhookConfiguration")
return err
}
for i, w := range vw.Webhooks {
// Updating CABundle only in case of an internal service reference
if w.ClientConfig.Service != nil {
vw.Webhooks[i].ClientConfig.CABundle = instance.Data[certSecretKey]
}
}
err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
return r.Update(context.TODO(), vw, &client.UpdateOptions{})
})
if err != nil {
r.Log.Error(err, "cannot update MutatingWebhookConfiguration webhooks CA bundle")
return err
}
return r.Update(context.TODO(), vw, &client.UpdateOptions{})
})
// Updating MutatingWebhookConfiguration CA bundle
err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
mw := &v1.MutatingWebhookConfiguration{}
err = r.Get(context.TODO(), types.NamespacedName{Name: "capsule-mutating-webhook-configuration"}, mw)
if err != nil {
r.Log.Error(err, "cannot retrieve MutatingWebhookConfiguration")
return err
}
for i, w := range mw.Webhooks {
// Updating CABundle only in case of an internal service reference
if w.ClientConfig.Service != nil {
mw.Webhooks[i].ClientConfig.CABundle = instance.Data[certSecretKey]
}
}
return r.Update(context.TODO(), mw, &client.UpdateOptions{})
})
if err != nil {
r.Log.Error(err, "cannot update MutatingWebhookConfiguration webhooks CA bundle")
return reconcile.Result{}, err
}
}
var res controllerutil.OperationResult
t := &corev1.Secret{ObjectMeta: instance.ObjectMeta}
res, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, t, func() error {
t.Data = instance.Data
return nil
})
if err != nil {
r.Log.Error(err, "cannot update Capsule TLS")
return reconcile.Result{}, err
}
if res == controllerutil.OperationResultUpdated {
r.Log.Info("Capsule CA has been updated, we need to trigger TLS update too")
tls := &corev1.Secret{}
err = r.Get(context.TODO(), types.NamespacedName{
Namespace: "capsule-system",
Name: tlsSecretName,
}, tls)
if err != nil {
r.Log.Error(err, "Capsule TLS Secret missing")
}
err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
_, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, tls, func() error {
tls.Data = map[string][]byte{}
return nil
})
return err
})
if err != nil {
r.Log.Error(err, "Cannot clean Capsule TLS Secret due to CA update")
return reconcile.Result{}, err
}
}
r.Log.Info("Reconciliation completed, processing back in " + rq.String())
return reconcile.Result{Requeue: true, RequeueAfter: rq}, nil
}

View File

@@ -1,9 +1,12 @@
/*
Copyright 2020 Clastix Labs.
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.
@@ -14,9 +17,9 @@ limitations under the License.
package secret
const (
Cert = "tls.crt"
PrivateKey = "tls.key"
certSecretKey = "tls.crt"
privateKeySecretKey = "tls.key"
CaSecretName = "capsule-ca"
TlsSecretName = "capsule-tls"
caSecretName = "capsule-ca"
tlsSecretName = "capsule-tls"
)

View File

@@ -1,9 +1,12 @@
/*
Copyright 2020 Clastix Labs.
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.
@@ -11,12 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package webhook
package secret
import (
"github.com/clastix/capsule/pkg/webhook/pvc"
)
func init() {
AddToWebhookServer = append(AddToWebhookServer, pvc.Add)
type MissingCaError struct {
}
func (MissingCaError) Error() string {
return "CA has not been created yet, please generate a new"
}

View File

@@ -0,0 +1,75 @@
/*
Copyright 2020 Clastix Labs.
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 secret
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"github.com/clastix/capsule/pkg/cert"
)
func getCertificateAuthority(client client.Client) (ca cert.Ca, err error) {
instance := &corev1.Secret{}
err = client.Get(context.TODO(), types.NamespacedName{
Namespace: "capsule-system",
Name: caSecretName,
}, instance)
if err != nil {
return nil, fmt.Errorf("missing secret %s, cannot reconcile", caSecretName)
}
if instance.Data == nil {
return nil, MissingCaError{}
}
ca, err = cert.NewCertificateAuthorityFromBytes(instance.Data[certSecretKey], instance.Data[privateKeySecretKey])
if err != nil {
return
}
return
}
func forOptionPerInstanceName(instanceName string) builder.ForOption {
return builder.WithPredicates(predicate.Funcs{
CreateFunc: func(event event.CreateEvent) bool {
return filterByName(event.Meta.GetName(), instanceName)
},
DeleteFunc: func(deleteEvent event.DeleteEvent) bool {
return filterByName(deleteEvent.Meta.GetName(), instanceName)
},
UpdateFunc: func(updateEvent event.UpdateEvent) bool {
return filterByName(updateEvent.MetaNew.GetName(), instanceName)
},
GenericFunc: func(genericEvent event.GenericEvent) bool {
return filterByName(genericEvent.Meta.GetName(), instanceName)
},
})
}
func filterByName(objName, desired string) bool {
return objName == desired
}

127
controllers/secret/tls.go Normal file
View File

@@ -0,0 +1,127 @@
/*
Copyright 2020 Clastix Labs.
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 secret
import (
"context"
"crypto/x509"
"encoding/pem"
"syscall"
"time"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/clastix/capsule/pkg/cert"
)
type TlsReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
}
func (r *TlsReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&corev1.Secret{}, forOptionPerInstanceName(tlsSecretName)).
Complete(r)
}
func (r TlsReconciler) Reconcile(request ctrl.Request) (ctrl.Result, error) {
var err error
r.Log = r.Log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
r.Log.Info("Reconciling TLS Secret")
// Fetch the Secret instance
instance := &corev1.Secret{}
err = r.Get(context.TODO(), request.NamespacedName, instance)
if err != nil {
// Error reading the object - requeue the request.
return reconcile.Result{}, err
}
var ca cert.Ca
var rq time.Duration
ca, err = getCertificateAuthority(r.Client)
if err != nil {
return reconcile.Result{}, err
}
var shouldCreate bool
for _, key := range []string{certSecretKey, privateKeySecretKey} {
if _, ok := instance.Data[key]; !ok {
shouldCreate = true
}
}
if shouldCreate {
r.Log.Info("Missing Capsule TLS certificate")
rq = 6 * 30 * 24 * time.Hour
opts := cert.NewCertOpts(time.Now().Add(rq), "capsule-webhook-service.capsule-system.svc")
crt, key, err := ca.GenerateCertificate(opts)
if err != nil {
r.Log.Error(err, "Cannot generate new TLS certificate")
return reconcile.Result{}, err
}
instance.Data = map[string][]byte{
certSecretKey: crt.Bytes(),
privateKeySecretKey: key.Bytes(),
}
} else {
var c *x509.Certificate
var b *pem.Block
b, _ = pem.Decode(instance.Data[certSecretKey])
c, err = x509.ParseCertificate(b.Bytes)
if err != nil {
r.Log.Error(err, "cannot parse Capsule TLS")
return reconcile.Result{}, err
}
rq = time.Duration(c.NotAfter.Unix()-time.Now().Unix()) * time.Second
if time.Now().After(c.NotAfter) {
r.Log.Info("Capsule TLS is expired, cleaning to obtain a new one")
instance.Data = map[string][]byte{}
}
}
var res controllerutil.OperationResult
t := &corev1.Secret{ObjectMeta: instance.ObjectMeta}
res, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, t, func() error {
t.Data = instance.Data
return nil
})
if err != nil {
r.Log.Error(err, "cannot update Capsule TLS")
return reconcile.Result{}, err
}
if instance.Name == tlsSecretName && res == controllerutil.OperationResultUpdated {
r.Log.Info("Capsule TLS certificates has been updated, we need to restart the Controller")
_ = syscall.Kill(syscall.Getpid(), syscall.SIGINT)
}
r.Log.Info("Reconciliation completed, processing back in " + rq.String())
return reconcile.Result{Requeue: true, RequeueAfter: rq}, nil
}

81
controllers/suite_test.go Normal file
View File

@@ -0,0 +1,81 @@
/*
Copyright 2020 Clastix Labs.
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 (
"path/filepath"
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
capsulev1alpha "github.com/clastix/capsule/api/v1alpha1"
// +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
func TestAPIs(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecsWithDefaultAndCustomReporters(t,
"Controller Suite",
[]Reporter{printer.NewlineReporter{}})
}
var _ = BeforeSuite(func(done Done) {
logf.SetLogger(zap.LoggerTo(GinkgoWriter, true))
By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
}
var err error
cfg, err = testEnv.Start()
Expect(err).ToNot(HaveOccurred())
Expect(cfg).ToNot(BeNil())
err = capsulev1alpha.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
// +kubebuilder:scaffold:scheme
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(err).ToNot(HaveOccurred())
Expect(k8sClient).ToNot(BeNil())
close(done)
}, 60)
var _ = AfterSuite(func() {
By("tearing down the test environment")
err := testEnv.Stop()
Expect(err).ToNot(HaveOccurred())
})

View File

@@ -1,9 +1,12 @@
/*
Copyright 2020 Clastix Labs.
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.
@@ -11,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package tenant
package controllers
import (
"context"
@@ -32,119 +35,83 @@ import (
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
capsulev1alpha1 "github.com/clastix/capsule/pkg/apis/capsule/v1alpha1"
capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1"
)
// Add creates a new Tenant Controller and adds it to the Manager. The Manager will set fields on the Controller
// and Start it when the Manager is Started.
func Add(mgr manager.Manager) error {
return add(mgr, &ReconcileTenant{
client: mgr.GetClient(),
scheme: mgr.GetScheme(),
logger: log.Log.WithName("controller_tenant"),
})
// TenantReconciler reconciles a Tenant object
type TenantReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
}
// add adds a new Controller to mgr with r as the reconcile.Reconciler
func add(mgr manager.Manager, r reconcile.Reconciler) error {
// Create a new controller
c, err := controller.New("tenant-controller", mgr, controller.Options{Reconciler: r})
if err != nil {
return err
}
// Watch for changes to primary resource Tenant
err = c.Watch(&source.Kind{Type: &capsulev1alpha1.Tenant{}}, &handler.EnqueueRequestForObject{})
if err != nil {
return err
}
// Watch for controlled resources
for _, r := range []runtime.Object{&networkingv1.NetworkPolicy{}, &corev1.LimitRange{}, &corev1.ResourceQuota{}, &rbacv1.RoleBinding{}} {
err = c.Watch(&source.Kind{Type: r}, &handler.EnqueueRequestForOwner{
IsController: true,
OwnerType: &capsulev1alpha1.Tenant{},
})
if err != nil {
return err
}
}
return nil
func (r *TenantReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&capsulev1alpha1.Tenant{}).
Owns(&networkingv1.NetworkPolicy{}).
Owns(&corev1.LimitRange{}).
Owns(&corev1.ResourceQuota{}).
Owns(&rbacv1.RoleBinding{}).
Complete(r)
}
// ReconcileTenant reconciles a Tenant object
type ReconcileTenant struct {
client client.Client
scheme *runtime.Scheme
logger logr.Logger
}
// Reconcile reads that state of the cluster for a Tenant object and makes changes based on the state read
// and what is in the Tenant.Spec
// The Controller will requeue the Request to be processed again if the returned error is non-nil or
// Result.Requeue is true, otherwise upon completion it will remove the work from the queue.
func (r *ReconcileTenant) Reconcile(request reconcile.Request) (reconcile.Result, error) {
r.logger = log.Log.WithName("controller_tenant").WithValues("Request.Name", request.Name)
func (r TenantReconciler) Reconcile(request ctrl.Request) (ctrl.Result, error) {
r.Log = r.Log.WithValues("Request.Name", request.Name)
// Fetch the Tenant instance
instance := &capsulev1alpha1.Tenant{}
err := r.client.Get(context.TODO(), request.NamespacedName, instance)
err := r.Get(context.TODO(), request.NamespacedName, instance)
if err != nil {
if errors.IsNotFound(err) {
r.logger.Info("Request object not found, could have been deleted after reconcile request")
r.Log.Info("Request object not found, could have been deleted after reconcile request")
return reconcile.Result{}, nil
}
r.logger.Error(err, "Error reading the object")
r.Log.Error(err, "Error reading the object")
return reconcile.Result{}, err
}
r.logger.Info("Starting processing of Network Policies", "items", len(instance.Spec.NetworkPolicies))
r.Log.Info("Starting processing of Network Policies", "items", len(instance.Spec.NetworkPolicies))
if err := r.syncNetworkPolicies(instance); err != nil {
r.logger.Error(err, "Cannot sync NetworkPolicy items")
r.Log.Error(err, "Cannot sync NetworkPolicy items")
return reconcile.Result{}, err
}
r.logger.Info("Starting processing of Node Selector")
r.Log.Info("Starting processing of Node Selector")
if err := r.ensureNodeSelector(instance); err != nil {
r.logger.Error(err, "Cannot sync Namespaces Node Selector items")
r.Log.Error(err, "Cannot sync Namespaces Node Selector items")
return reconcile.Result{}, err
}
r.logger.Info("Starting processing of Limit Ranges", "items", len(instance.Spec.LimitRanges))
r.Log.Info("Starting processing of Limit Ranges", "items", len(instance.Spec.LimitRanges))
if err := r.syncLimitRanges(instance); err != nil {
r.logger.Error(err, "Cannot sync LimitRange items")
r.Log.Error(err, "Cannot sync LimitRange items")
return reconcile.Result{}, err
}
r.logger.Info("Starting processing of Resource Quotas", "items", len(instance.Spec.ResourceQuota))
r.Log.Info("Starting processing of Resource Quotas", "items", len(instance.Spec.ResourceQuota))
if err := r.syncResourceQuotas(instance); err != nil {
r.logger.Error(err, "Cannot sync ResourceQuota items")
r.Log.Error(err, "Cannot sync ResourceQuota items")
return reconcile.Result{}, err
}
r.logger.Info("Ensuring RoleBinding for owner")
r.Log.Info("Ensuring RoleBinding for owner")
if err := r.ownerRoleBinding(instance); err != nil {
r.logger.Error(err, "Cannot sync owner RoleBinding")
r.Log.Error(err, "Cannot sync owner RoleBinding")
return reconcile.Result{}, err
}
r.logger.Info("Tenant reconciling completed")
return reconcile.Result{}, nil
r.Log.Info("Tenant reconciling completed")
return ctrl.Result{}, nil
}
// pruningResources is taking care of removing the no more requested sub-resources as LimitRange, ResourceQuota or
// NetworkPolicy using the "notin" LabelSelector to perform an outer-join removal.
func (r *ReconcileTenant) pruningResources(ns string, keys []string, obj runtime.Object) error {
func (r *TenantReconciler) pruningResources(ns string, keys []string, obj runtime.Object) error {
capsuleLabel, err := capsulev1alpha1.GetTypeLabel(obj)
if err != nil {
return err
@@ -153,9 +120,9 @@ func (r *ReconcileTenant) pruningResources(ns string, keys []string, obj runtime
if err != nil {
return err
}
r.logger.Info("Pruning objects with label selector " + req.String())
r.Log.Info("Pruning objects with label selector " + req.String())
err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
return r.client.DeleteAllOf(context.TODO(), obj, &client.DeleteAllOfOptions{
return r.DeleteAllOf(context.TODO(), obj, &client.DeleteAllOfOptions{
ListOptions: client.ListOptions{
LabelSelector: labels.NewSelector().Add(*req),
Namespace: ns,
@@ -173,7 +140,7 @@ func (r *ReconcileTenant) pruningResources(ns string, keys []string, obj runtime
// Serial ResourceQuota processing is expensive: using Go routines we can speed it up.
// In case of multiple errors these are logged properly, returning a generic error since we have to repush back the
// reconciliation loop.
func (r *ReconcileTenant) resourceQuotasUpdate(resourceName corev1.ResourceName, qt resource.Quantity, list ...corev1.ResourceQuota) (err error) {
func (r *TenantReconciler) resourceQuotasUpdate(resourceName corev1.ResourceName, qt resource.Quantity, list ...corev1.ResourceQuota) (err error) {
ch := make(chan error, len(list))
wg := &sync.WaitGroup{}
@@ -184,7 +151,7 @@ func (r *ReconcileTenant) resourceQuotasUpdate(resourceName corev1.ResourceName,
ch <- retry.RetryOnConflict(retry.DefaultBackoff, func() error {
// Retrieving from the cache the actual ResourceQuota
found := &corev1.ResourceQuota{}
_ = r.client.Get(context.TODO(), types.NamespacedName{Namespace: rq.Namespace, Name: rq.Name}, found)
_ = r.Get(context.TODO(), types.NamespacedName{Namespace: rq.Namespace, Name: rq.Name}, found)
// Ensuring annotation map is there to avoid uninitialized map error and
// assigning the overall usage
if found.Annotations == nil {
@@ -194,7 +161,7 @@ func (r *ReconcileTenant) resourceQuotasUpdate(resourceName corev1.ResourceName,
found.Annotations[capsulev1alpha1.UsedQuotaFor(resourceName)] = qt.String()
// Updating the Resource according to the qt.Cmp result
found.Spec.Hard = rq.Spec.Hard
return r.client.Update(context.TODO(), found, &client.UpdateOptions{})
return r.Update(context.TODO(), found, &client.UpdateOptions{})
})
}
@@ -209,7 +176,7 @@ func (r *ReconcileTenant) resourceQuotasUpdate(resourceName corev1.ResourceName,
// We had an error and we mark the whole transaction as failed
// to process it another time acording to the Tenant controller back-off factor.
err = fmt.Errorf("update of outer ResourceQuota items has failed")
r.logger.Error(err, "Cannot update outer ResourceQuotas", "resourceName", resourceName.String())
r.Log.Error(err, "Cannot update outer ResourceQuotas", "resourceName", resourceName.String())
}
}
return
@@ -223,7 +190,7 @@ func (r *ReconcileTenant) resourceQuotasUpdate(resourceName corev1.ResourceName,
// .Status.Used value as the .Hard value.
// This will trigger a following reconciliation but that's ok: the mutateFn will re-use the same business logic, letting
// the mutateFn along with the CreateOrUpdate to don't perform the update since resources are identical.
func (r *ReconcileTenant) syncResourceQuotas(tenant *capsulev1alpha1.Tenant) error {
func (r *TenantReconciler) syncResourceQuotas(tenant *capsulev1alpha1.Tenant) error {
// getting requested ResourceQuota keys
keys := make([]string, 0, len(tenant.Spec.ResourceQuota))
for i := range tenant.Spec.ResourceQuota {
@@ -256,27 +223,27 @@ func (r *ReconcileTenant) syncResourceQuotas(tenant *capsulev1alpha1.Tenant) err
},
},
}
res, err := controllerutil.CreateOrUpdate(context.TODO(), r.client, target, func() (err error) {
res, err := controllerutil.CreateOrUpdate(context.TODO(), r.Client, target, func() (err error) {
// Requirement to list ResourceQuota of the current Tenant
tr, err := labels.NewRequirement(tenantLabel, selection.Equals, []string{tenant.Name})
if err != nil {
r.logger.Error(err, "Cannot build ResourceQuota Tenant requirement")
r.Log.Error(err, "Cannot build ResourceQuota Tenant requirement")
}
// Requirement to list ResourceQuota for the current index
ir, err := labels.NewRequirement(typeLabel, selection.Equals, []string{strconv.Itoa(i)})
if err != nil {
r.logger.Error(err, "Cannot build ResourceQuota index requirement")
r.Log.Error(err, "Cannot build ResourceQuota index requirement")
}
// Listing all the ResourceQuota according to the said requirements.
// These are required since Capsule is going to sum all the used quota to
// sum them and get the Tenant one.
rql := &corev1.ResourceQuotaList{}
err = r.client.List(context.TODO(), rql, &client.ListOptions{
err = r.List(context.TODO(), rql, &client.ListOptions{
LabelSelector: labels.NewSelector().Add(*tr).Add(*ir),
})
if err != nil {
r.logger.Error(err, "Cannot list ResourceQuota", "tenantFilter", tr.String(), "indexFilter", ir.String())
r.Log.Error(err, "Cannot list ResourceQuota", "tenantFilter", tr.String(), "indexFilter", ir.String())
return err
}
@@ -286,14 +253,14 @@ func (r *ReconcileTenant) syncResourceQuotas(tenant *capsulev1alpha1.Tenant) err
// For this case, we're going to block the Quota setting the Hard as the
// used one.
for rn, rq := range q.Hard {
r.logger.Info("Desired hard " + rn.String() + " quota is " + rq.String())
r.Log.Info("Desired hard " + rn.String() + " quota is " + rq.String())
// Getting the whole usage across all the Tenant Namespaces
var qt resource.Quantity
for _, rq := range rql.Items {
qt.Add(rq.Status.Used[rn])
}
r.logger.Info("Computed " + rn.String() + " quota for the whole Tenant is " + qt.String())
r.Log.Info("Computed " + rn.String() + " quota for the whole Tenant is " + qt.String())
switch qt.Cmp(q.Hard[rn]) {
case 0:
@@ -324,13 +291,13 @@ func (r *ReconcileTenant) syncResourceQuotas(tenant *capsulev1alpha1.Tenant) err
target.Spec = q
}
if err := r.resourceQuotasUpdate(rn, qt, rql.Items...); err != nil {
r.logger.Error(err, "cannot proceed with outer ResourceQuota")
r.Log.Error(err, "cannot proceed with outer ResourceQuota")
return err
}
}
return controllerutil.SetControllerReference(tenant, target, r.scheme)
return controllerutil.SetControllerReference(tenant, target, r.Scheme)
})
r.logger.Info("Resource Quota sync result: "+string(res), "name", target.Name, "namespace", target.Namespace)
r.Log.Info("Resource Quota sync result: "+string(res), "name", target.Name, "namespace", target.Namespace)
if err != nil {
return err
}
@@ -341,7 +308,7 @@ func (r *ReconcileTenant) syncResourceQuotas(tenant *capsulev1alpha1.Tenant) err
}
// Ensuring all the LimitRange are applied to each Namespace handled by the Tenant.
func (r *ReconcileTenant) syncLimitRanges(tenant *capsulev1alpha1.Tenant) error {
func (r *TenantReconciler) syncLimitRanges(tenant *capsulev1alpha1.Tenant) error {
// getting requested LimitRange keys
keys := make([]string, 0, len(tenant.Spec.LimitRanges))
for i := range tenant.Spec.LimitRanges {
@@ -369,15 +336,15 @@ func (r *ReconcileTenant) syncLimitRanges(tenant *capsulev1alpha1.Tenant) error
Namespace: ns,
},
}
res, err := controllerutil.CreateOrUpdate(context.TODO(), r.client, t, func() (err error) {
res, err := controllerutil.CreateOrUpdate(context.TODO(), r.Client, t, func() (err error) {
t.ObjectMeta.Labels = map[string]string{
tl: tenant.Name,
ll: strconv.Itoa(i),
}
t.Spec = spec
return controllerutil.SetControllerReference(tenant, t, r.scheme)
return controllerutil.SetControllerReference(tenant, t, r.Scheme)
})
r.logger.Info("LimitRange sync result: "+string(res), "name", t.Name, "namespace", t.Namespace)
r.Log.Info("LimitRange sync result: "+string(res), "name", t.Name, "namespace", t.Namespace)
if err != nil {
return err
}
@@ -388,7 +355,7 @@ func (r *ReconcileTenant) syncLimitRanges(tenant *capsulev1alpha1.Tenant) error
}
// Ensuring all the NetworkPolicies are applied to each Namespace handled by the Tenant.
func (r *ReconcileTenant) syncNetworkPolicies(tenant *capsulev1alpha1.Tenant) error {
func (r *TenantReconciler) syncNetworkPolicies(tenant *capsulev1alpha1.Tenant) error {
// getting requested NetworkPolicy keys
keys := make([]string, 0, len(tenant.Spec.NetworkPolicies))
for i := range tenant.Spec.NetworkPolicies {
@@ -420,11 +387,11 @@ func (r *ReconcileTenant) syncNetworkPolicies(tenant *capsulev1alpha1.Tenant) er
},
},
}
res, err := controllerutil.CreateOrUpdate(context.TODO(), r.client, t, func() (err error) {
res, err := controllerutil.CreateOrUpdate(context.TODO(), r.Client, t, func() (err error) {
t.Spec = spec
return controllerutil.SetControllerReference(tenant, t, r.scheme)
return controllerutil.SetControllerReference(tenant, t, r.Scheme)
})
r.logger.Info("Network Policy sync result: "+string(res), "name", t.Name, "namespace", t.Namespace)
r.Log.Info("Network Policy sync result: "+string(res), "name", t.Name, "namespace", t.Namespace)
if err != nil {
return err
}
@@ -438,7 +405,7 @@ func (r *ReconcileTenant) syncNetworkPolicies(tenant *capsulev1alpha1.Tenant) er
// Since RBAC is based on deny all first, some specific actions like editing Capsule resources are going to be blocked
// via Dynamic Admission Webhooks.
// TODO(prometherion): we could create a capsule:admin role rather than hitting webhooks for each action
func (r *ReconcileTenant) ownerRoleBinding(tenant *capsulev1alpha1.Tenant) error {
func (r *TenantReconciler) ownerRoleBinding(tenant *capsulev1alpha1.Tenant) error {
// getting RoleBinding label for the mutateFn
tl, err := capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{})
if err != nil {
@@ -463,7 +430,7 @@ func (r *ReconcileTenant) ownerRoleBinding(tenant *capsulev1alpha1.Tenant) error
rbl[types.NamespacedName{Namespace: i, Name: "namespace:deleter"}] = rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "ClusterRole",
Name: "namespace:deleter",
Name: "capsule-namespace:deleter",
}
}
@@ -476,13 +443,13 @@ func (r *ReconcileTenant) ownerRoleBinding(tenant *capsulev1alpha1.Tenant) error
}
var res controllerutil.OperationResult
res, err = controllerutil.CreateOrUpdate(context.TODO(), r.client, target, func() (err error) {
res, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, target, func() (err error) {
target.ObjectMeta.Labels = l
target.Subjects = s
target.RoleRef = rr
return controllerutil.SetControllerReference(tenant, target, r.scheme)
return controllerutil.SetControllerReference(tenant, target, r.Scheme)
})
r.logger.Info("Role Binding sync result: "+string(res), "name", target.Name, "namespace", target.Namespace)
r.Log.Info("Role Binding sync result: "+string(res), "name", target.Name, "namespace", target.Namespace)
if err != nil {
return err
}
@@ -490,7 +457,7 @@ func (r *ReconcileTenant) ownerRoleBinding(tenant *capsulev1alpha1.Tenant) error
return nil
}
func (r *ReconcileTenant) ensureNodeSelector(tenant *capsulev1alpha1.Tenant) (err error) {
func (r *TenantReconciler) ensureNodeSelector(tenant *capsulev1alpha1.Tenant) (err error) {
if tenant.Spec.NodeSelector == nil {
return
}
@@ -508,7 +475,7 @@ func (r *ReconcileTenant) ensureNodeSelector(tenant *capsulev1alpha1.Tenant) (er
}
var res controllerutil.OperationResult
res, err = controllerutil.CreateOrUpdate(context.TODO(), r.client, ns, func() error {
res, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, ns, func() error {
if ns.Annotations == nil {
ns.Annotations = make(map[string]string)
}
@@ -519,7 +486,7 @@ func (r *ReconcileTenant) ensureNodeSelector(tenant *capsulev1alpha1.Tenant) (er
ns.Annotations["scheduler.alpha.kubernetes.io/node-selector"] = strings.Join(selector, ",")
return nil
})
r.logger.Info("Namespace Node sync result: "+string(res), "name", ns.Name)
r.Log.Info("Namespace Node sync result: "+string(res), "name", ns.Name)
if err != nil {
return err
}

View File

@@ -1,710 +0,0 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: tenants.capsule.clastix.io
spec:
group: capsule.clastix.io
names:
kind: Tenant
listKind: TenantList
plural: tenants
singular: tenant
scope: Cluster
versions:
- additionalPrinterColumns:
- description: The max amount of Namespaces can be created
jsonPath: .spec.namespaceQuota
name: Namespace quota
type: integer
- description: The total amount of Namespaces in use
jsonPath: .status.size
name: Namespace count
type: integer
name: v1alpha1
schema:
openAPIV3Schema:
description: Tenant is the Schema for the tenants 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: TenantSpec defines the desired state of Tenant
properties:
ingressClasses:
items:
type: string
type: array
limitRanges:
items:
description: LimitRangeSpec defines a min/max usage limit for resources
that match on kind.
properties:
limits:
description: Limits is the list of LimitRangeItem objects that
are enforced.
items:
description: LimitRangeItem defines a min/max usage limit
for any resource that matches on kind.
properties:
default:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: Default resource requirement limit value
by resource name if resource limit is omitted.
type: object
defaultRequest:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: DefaultRequest is the default resource requirement
request value by resource name if resource request is
omitted.
type: object
max:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: Max usage constraints on this kind by resource
name.
type: object
maxLimitRequestRatio:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: MaxLimitRequestRatio if specified, the named
resource must have a request and limit that are both
non-zero where limit divided by request is less than
or equal to the enumerated value; this represents the
max burst for the named resource.
type: object
min:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: Min usage constraints on this kind by resource
name.
type: object
type:
description: Type of resource that this limit applies
to.
type: string
required:
- type
type: object
type: array
required:
- limits
type: object
type: array
namespaceQuota:
minimum: 1
type: integer
networkPolicies:
items:
description: NetworkPolicySpec provides the specification of a NetworkPolicy
properties:
egress:
description: List of egress rules to be applied to the selected
pods. Outgoing traffic is allowed if there are no NetworkPolicies
selecting the pod (and cluster policy otherwise allows the
traffic), OR if the traffic matches at least one egress rule
across all of the NetworkPolicy objects whose podSelector
matches the pod. If this field is empty then this NetworkPolicy
limits all outgoing traffic (and serves solely to ensure that
the pods it selects are isolated by default). This field is
beta-level in 1.8
items:
description: NetworkPolicyEgressRule describes a particular
set of traffic that is allowed out of pods matched by a
NetworkPolicySpec's podSelector. The traffic must match
both ports and to. This type is beta-level in 1.8
properties:
ports:
description: List of destination ports for outgoing traffic.
Each item in this list is combined using a logical OR.
If this field is empty or missing, this rule matches
all ports (traffic not restricted by port). If this
field is present and contains at least one item, then
this rule allows traffic only if the traffic matches
at least one port in the list.
items:
description: NetworkPolicyPort describes a port to allow
traffic on
properties:
port:
anyOf:
- type: integer
- type: string
description: The port on the given protocol. This
can either be a numerical or named port on a pod.
If this field is not provided, this matches all
port names and numbers.
x-kubernetes-int-or-string: true
protocol:
description: The protocol (TCP, UDP, or SCTP) which
traffic must match. If not specified, this field
defaults to TCP.
type: string
type: object
type: array
to:
description: List of destinations for outgoing traffic
of pods selected for this rule. Items in this list are
combined using a logical OR operation. If this field
is empty or missing, this rule matches all destinations
(traffic not restricted by destination). If this field
is present and contains at least one item, this rule
allows traffic only if the traffic matches at least
one item in the to list.
items:
description: NetworkPolicyPeer describes a peer to allow
traffic from. Only certain combinations of fields
are allowed
properties:
ipBlock:
description: IPBlock defines policy on a particular
IPBlock. If this field is set then neither of
the other fields can be.
properties:
cidr:
description: CIDR is a string representing the
IP Block Valid examples are "192.168.1.1/24"
or "2001:db9::/64"
type: string
except:
description: Except is a slice of CIDRs that
should not be included within an IP Block
Valid examples are "192.168.1.1/24" or "2001:db9::/64"
Except values will be rejected if they are
outside the CIDR range
items:
type: string
type: array
required:
- cidr
type: object
namespaceSelector:
description: "Selects Namespaces using cluster-scoped
labels. This field follows standard label selector
semantics; if present but empty, it selects all
namespaces. \n If PodSelector is also set, then
the NetworkPolicyPeer as a whole selects the Pods
matching PodSelector in the Namespaces selected
by NamespaceSelector. Otherwise it selects all
Pods in the Namespaces selected by NamespaceSelector."
properties:
matchExpressions:
description: matchExpressions is a list of label
selector requirements. The requirements are
ANDed.
items:
description: A label selector requirement
is a selector that contains values, a key,
and an operator that relates the key and
values.
properties:
key:
description: key is the label key that
the selector applies to.
type: string
operator:
description: operator represents a key's
relationship to a set of values. Valid
operators are In, NotIn, Exists and
DoesNotExist.
type: string
values:
description: values is an array of string
values. If the operator is In or NotIn,
the values array must be non-empty.
If the operator is Exists or DoesNotExist,
the values array must be empty. This
array is replaced during a strategic
merge patch.
items:
type: string
type: array
required:
- key
- operator
type: object
type: array
matchLabels:
additionalProperties:
type: string
description: matchLabels is a map of {key,value}
pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions,
whose key field is "key", the operator is
"In", and the values array contains only "value".
The requirements are ANDed.
type: object
type: object
podSelector:
description: "This is a label selector which selects
Pods. This field follows standard label selector
semantics; if present but empty, it selects all
pods. \n If NamespaceSelector is also set, then
the NetworkPolicyPeer as a whole selects the Pods
matching PodSelector in the Namespaces selected
by NamespaceSelector. Otherwise it selects the
Pods matching PodSelector in the policy's own
Namespace."
properties:
matchExpressions:
description: matchExpressions is a list of label
selector requirements. The requirements are
ANDed.
items:
description: A label selector requirement
is a selector that contains values, a key,
and an operator that relates the key and
values.
properties:
key:
description: key is the label key that
the selector applies to.
type: string
operator:
description: operator represents a key's
relationship to a set of values. Valid
operators are In, NotIn, Exists and
DoesNotExist.
type: string
values:
description: values is an array of string
values. If the operator is In or NotIn,
the values array must be non-empty.
If the operator is Exists or DoesNotExist,
the values array must be empty. This
array is replaced during a strategic
merge patch.
items:
type: string
type: array
required:
- key
- operator
type: object
type: array
matchLabels:
additionalProperties:
type: string
description: matchLabels is a map of {key,value}
pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions,
whose key field is "key", the operator is
"In", and the values array contains only "value".
The requirements are ANDed.
type: object
type: object
type: object
type: array
type: object
type: array
ingress:
description: List of ingress rules to be applied to the selected
pods. Traffic is allowed to a pod if there are no NetworkPolicies
selecting the pod (and cluster policy otherwise allows the
traffic), OR if the traffic source is the pod's local node,
OR if the traffic matches at least one ingress rule across
all of the NetworkPolicy objects whose podSelector matches
the pod. If this field is empty then this NetworkPolicy does
not allow any traffic (and serves solely to ensure that the
pods it selects are isolated by default)
items:
description: NetworkPolicyIngressRule describes a particular
set of traffic that is allowed to the pods matched by a
NetworkPolicySpec's podSelector. The traffic must match
both ports and from.
properties:
from:
description: List of sources which should be able to access
the pods selected for this rule. Items in this list
are combined using a logical OR operation. If this field
is empty or missing, this rule matches all sources (traffic
not restricted by source). If this field is present
and contains at least one item, this rule allows traffic
only if the traffic matches at least one item in the
from list.
items:
description: NetworkPolicyPeer describes a peer to allow
traffic from. Only certain combinations of fields
are allowed
properties:
ipBlock:
description: IPBlock defines policy on a particular
IPBlock. If this field is set then neither of
the other fields can be.
properties:
cidr:
description: CIDR is a string representing the
IP Block Valid examples are "192.168.1.1/24"
or "2001:db9::/64"
type: string
except:
description: Except is a slice of CIDRs that
should not be included within an IP Block
Valid examples are "192.168.1.1/24" or "2001:db9::/64"
Except values will be rejected if they are
outside the CIDR range
items:
type: string
type: array
required:
- cidr
type: object
namespaceSelector:
description: "Selects Namespaces using cluster-scoped
labels. This field follows standard label selector
semantics; if present but empty, it selects all
namespaces. \n If PodSelector is also set, then
the NetworkPolicyPeer as a whole selects the Pods
matching PodSelector in the Namespaces selected
by NamespaceSelector. Otherwise it selects all
Pods in the Namespaces selected by NamespaceSelector."
properties:
matchExpressions:
description: matchExpressions is a list of label
selector requirements. The requirements are
ANDed.
items:
description: A label selector requirement
is a selector that contains values, a key,
and an operator that relates the key and
values.
properties:
key:
description: key is the label key that
the selector applies to.
type: string
operator:
description: operator represents a key's
relationship to a set of values. Valid
operators are In, NotIn, Exists and
DoesNotExist.
type: string
values:
description: values is an array of string
values. If the operator is In or NotIn,
the values array must be non-empty.
If the operator is Exists or DoesNotExist,
the values array must be empty. This
array is replaced during a strategic
merge patch.
items:
type: string
type: array
required:
- key
- operator
type: object
type: array
matchLabels:
additionalProperties:
type: string
description: matchLabels is a map of {key,value}
pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions,
whose key field is "key", the operator is
"In", and the values array contains only "value".
The requirements are ANDed.
type: object
type: object
podSelector:
description: "This is a label selector which selects
Pods. This field follows standard label selector
semantics; if present but empty, it selects all
pods. \n If NamespaceSelector is also set, then
the NetworkPolicyPeer as a whole selects the Pods
matching PodSelector in the Namespaces selected
by NamespaceSelector. Otherwise it selects the
Pods matching PodSelector in the policy's own
Namespace."
properties:
matchExpressions:
description: matchExpressions is a list of label
selector requirements. The requirements are
ANDed.
items:
description: A label selector requirement
is a selector that contains values, a key,
and an operator that relates the key and
values.
properties:
key:
description: key is the label key that
the selector applies to.
type: string
operator:
description: operator represents a key's
relationship to a set of values. Valid
operators are In, NotIn, Exists and
DoesNotExist.
type: string
values:
description: values is an array of string
values. If the operator is In or NotIn,
the values array must be non-empty.
If the operator is Exists or DoesNotExist,
the values array must be empty. This
array is replaced during a strategic
merge patch.
items:
type: string
type: array
required:
- key
- operator
type: object
type: array
matchLabels:
additionalProperties:
type: string
description: matchLabels is a map of {key,value}
pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions,
whose key field is "key", the operator is
"In", and the values array contains only "value".
The requirements are ANDed.
type: object
type: object
type: object
type: array
ports:
description: List of ports which should be made accessible
on the pods selected for this rule. Each item in this
list is combined using a logical OR. If this field is
empty or missing, this rule matches all ports (traffic
not restricted by port). If this field is present and
contains at least one item, then this rule allows traffic
only if the traffic matches at least one port in the
list.
items:
description: NetworkPolicyPort describes a port to allow
traffic on
properties:
port:
anyOf:
- type: integer
- type: string
description: The port on the given protocol. This
can either be a numerical or named port on a pod.
If this field is not provided, this matches all
port names and numbers.
x-kubernetes-int-or-string: true
protocol:
description: The protocol (TCP, UDP, or SCTP) which
traffic must match. If not specified, this field
defaults to TCP.
type: string
type: object
type: array
type: object
type: array
podSelector:
description: Selects the pods to which this NetworkPolicy object
applies. The array of ingress rules is applied to any pods
selected by this field. Multiple network policies can select
the same set of pods. In this case, the ingress rules for
each are combined additively. This field is NOT optional and
follows standard label selector semantics. An empty podSelector
matches all pods in this namespace.
properties:
matchExpressions:
description: matchExpressions is a list of label selector
requirements. The requirements are ANDed.
items:
description: A label selector requirement is a selector
that contains values, a key, and an operator that relates
the key and values.
properties:
key:
description: key is the label key that the selector
applies to.
type: string
operator:
description: operator represents a key's relationship
to a set of values. Valid operators are In, NotIn,
Exists and DoesNotExist.
type: string
values:
description: values is an array of string values.
If the operator is In or NotIn, the values array
must be non-empty. If the operator is Exists or
DoesNotExist, the values array must be empty. This
array is replaced during a strategic merge patch.
items:
type: string
type: array
required:
- key
- operator
type: object
type: array
matchLabels:
additionalProperties:
type: string
description: matchLabels is a map of {key,value} pairs.
A single {key,value} in the matchLabels map is equivalent
to an element of matchExpressions, whose key field is
"key", the operator is "In", and the values array contains
only "value". The requirements are ANDed.
type: object
type: object
policyTypes:
description: List of rule types that the NetworkPolicy relates
to. Valid options are "Ingress", "Egress", or "Ingress,Egress".
If this field is not specified, it will default based on the
existence of Ingress or Egress rules; policies that contain
an Egress section are assumed to affect Egress, and all policies
(whether or not they contain an Ingress section) are assumed
to affect Ingress. If you want to write an egress-only policy,
you must explicitly specify policyTypes [ "Egress" ]. Likewise,
if you want to write a policy that specifies that no egress
is allowed, you must specify a policyTypes value that include
"Egress" (since such a policy would not include an Egress
section and would otherwise default to just [ "Ingress" ]).
This field is beta-level in 1.8
items:
description: Policy Type string describes the NetworkPolicy
type This type is beta-level in 1.8
type: string
type: array
required:
- podSelector
type: object
type: array
nodeSelector:
additionalProperties:
type: string
type: object
owner:
type: string
resourceQuotas:
items:
description: ResourceQuotaSpec defines the desired hard limits to
enforce for Quota.
properties:
hard:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: 'hard is the set of desired hard limits for each
named resource. More info: https://kubernetes.io/docs/concepts/policy/resource-quotas/'
type: object
scopeSelector:
description: scopeSelector is also a collection of filters like
scopes that must match each object tracked by a quota but
expressed using ScopeSelectorOperator in combination with
possible values. For a resource to match, both scopes AND
scopeSelector (if specified in spec), must be matched.
properties:
matchExpressions:
description: A list of scope selector requirements by scope
of the resources.
items:
description: A scoped-resource selector requirement is
a selector that contains values, a scope name, and an
operator that relates the scope name and values.
properties:
operator:
description: Represents a scope's relationship to
a set of values. Valid operators are In, NotIn,
Exists, DoesNotExist.
type: string
scopeName:
description: The name of the scope that the selector
applies to.
type: string
values:
description: An array of string values. If the operator
is In or NotIn, the values array must be non-empty.
If the operator is Exists or DoesNotExist, the values
array must be empty. This array is replaced during
a strategic merge patch.
items:
type: string
type: array
required:
- operator
- scopeName
type: object
type: array
type: object
scopes:
description: A collection of filters that must match each object
tracked by a quota. If not specified, the quota matches all
objects.
items:
description: A ResourceQuotaScope defines a filter that must
match each object tracked by a quota
type: string
type: array
type: object
type: array
storageClasses:
items:
type: string
type: array
required:
- ingressClasses
- limitRanges
- namespaceQuota
- owner
- storageClasses
type: object
status:
description: TenantStatus defines the observed state of Tenant
properties:
groups:
items:
type: string
type: array
namespaces:
items:
type: string
type: array
size:
type: integer
users:
items:
type: string
type: array
required:
- size
type: object
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -1,96 +0,0 @@
apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
name: capsule
webhooks:
- name: owner.namespace.capsule.clastix.io
failurePolicy: Fail
rules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["namespaces"]
clientConfig:
# use url if you're developing locally
# url: https://<FIXME>.ngrok.io/mutate-v1-namespace-owner-reference
caBundle:
service:
namespace: capsule-system
name: capsule
path: /mutate-v1-namespace-owner-reference
- name: quota.namespace.capsule.clastix.io
failurePolicy: Fail
rules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["namespaces"]
clientConfig:
# use url if you're developing locally
# url: https://<FIXME>.ngrok.io/validate-v1-namespace-quota
caBundle:
service:
namespace: capsule-system
name: capsule
path: /validate-v1-namespace-quota
- name: validating.network-policy.capsule.clastix.io
failurePolicy: Fail
rules:
- apiGroups: ["networking.k8s.io"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE", "DELETE"]
resources: ["networkpolicies"]
clientConfig:
# use url if you're developing locally
# url: https://<FIXME>.ngrok.io/validating-v1-network-policy
caBundle:
service:
namespace: capsule-system
name: capsule
path: /validating-v1-network-policy
- name: pvc.capsule.clastix.io
failurePolicy: Fail
rules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["persistentvolumeclaims"]
clientConfig:
# use url if you're developing locally
# url: https://<FIXME>.ngrok.io/validating-v1-pvc
caBundle:
service:
namespace: capsule-system
name: capsule
path: /validating-v1-pvc
- name: extensions.ingress.capsule.clastix.io
failurePolicy: Fail
rules:
- apiGroups: ["extensions"]
apiVersions: ["v1beta1"]
operations: ["CREATE", "UPDATE"]
resources: ["ingresses"]
clientConfig:
# use url if you're developing locally
# url: https://<FIXME>.ngrok.io/validating-v1-extensions-ingress
caBundle:
service:
namespace: capsule-system
name: capsule
path: /validating-v1-extensions-ingress
- name: networking.ingress.capsule.clastix.io
failurePolicy: Fail
rules:
- apiGroups: ["networking.k8s.io"]
apiVersions: ["v1beta1"]
operations: ["CREATE", "UPDATE"]
resources: ["ingresses"]
clientConfig:
# use url if you're developing locally
# url: https://<FIXME>.ngrok.io/validating-v1-networking-ingress
caBundle:
service:
namespace: capsule-system
name: capsule
path: /validating-v1-networking-ingress

View File

@@ -1,38 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: capsule
namespace: capsule-system
spec:
replicas: 1
selector:
matchLabels:
name: capsule
template:
metadata:
labels:
name: capsule
spec:
serviceAccountName: capsule
containers:
- name: capsule
image: quay.io/clastix/capsule:latest
command:
- capsule
imagePullPolicy: IfNotPresent
env:
- name: WATCH_NAMESPACE
value: ""
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: OPERATOR_NAME
value: "capsule"
volumeMounts:
- name: tls
mountPath: /tmp/k8s-webhook-server/serving-certs
volumes:
- name: tls
secret:
secretName: capsule-tls

View File

@@ -1,44 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
creationTimestamp: null
name: capsule
rules:
- apiGroups:
- admissionregistration.k8s.io
resources:
- mutatingwebhookconfigurations
verbs:
- get
- list
- watch
- update
- patch
- apiGroups:
- ""
resources:
- limitranges
- resourcequotas
- namespaces
- secrets
verbs:
- create
- delete
- deletecollection
- get
- list
- patch
- update
- watch
- apiGroups:
- capsule.clastix.io
resources:
- '*'
verbs:
- create
- delete
- get
- list
- patch
- update
- watch

View File

@@ -1,25 +0,0 @@
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: capsule-cluster-admin
subjects:
- kind: ServiceAccount
name: capsule
namespace: capsule-system
roleRef:
kind: ClusterRole
name: admin
apiGroup: rbac.authorization.k8s.io
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: capsule
subjects:
- kind: ServiceAccount
name: capsule
namespace: capsule-system
roleRef:
kind: ClusterRole
name: capsule
apiGroup: rbac.authorization.k8s.io

View File

@@ -1,7 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
labels:
app: capsule
name: capsule-ca
namespace: capsule-system

View File

@@ -1,7 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
labels:
app: capsule
name: capsule-tls
namespace: capsule-system

View File

@@ -1,16 +0,0 @@
apiVersion: v1
kind: Service
metadata:
labels:
app: capsule
name: capsule
namespace: capsule-system
spec:
ports:
- name: https
port: 443
protocol: TCP
targetPort: 443
selector:
name: capsule
type: ClusterIP

View File

@@ -1,5 +0,0 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: capsule
namespace: capsule-system

13
go.mod
View File

@@ -4,16 +4,11 @@ go 1.13
require (
github.com/go-logr/logr v0.1.0
github.com/operator-framework/operator-sdk v0.18.1
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.5.1
github.com/onsi/ginkgo v1.11.0
github.com/onsi/gomega v1.8.1
github.com/stretchr/testify v1.4.0
k8s.io/api v0.18.2
k8s.io/apimachinery v0.18.2
k8s.io/client-go v12.0.0+incompatible
k8s.io/client-go v0.18.2
sigs.k8s.io/controller-runtime v0.6.0
)
replace (
github.com/Azure/go-autorest => github.com/Azure/go-autorest v13.3.2+incompatible // Required by OLM
k8s.io/client-go => k8s.io/client-go v0.18.2 // Required by prometheus-operator
)

840
go.sum

File diff suppressed because it is too large Load Diff

1
hack/.gitignore vendored
View File

@@ -1 +0,0 @@
*.kubeconfig

View File

@@ -1,20 +1,15 @@
/*
Copyright 2020 Clastix Labs.
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 webhook
import "github.com/clastix/capsule/pkg/webhook/owner_reference"
func init() {
AddToWebhookServer = append(AddToWebhookServer, owner_reference.Add)
}
*/

149
main.go Normal file
View File

@@ -0,0 +1,149 @@
/*
Copyright 2020 Clastix Labs.
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 (
"flag"
"fmt"
"os"
goRuntime "runtime"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1"
"github.com/clastix/capsule/controllers"
"github.com/clastix/capsule/controllers/secret"
"github.com/clastix/capsule/pkg/indexer"
"github.com/clastix/capsule/pkg/webhook"
"github.com/clastix/capsule/pkg/webhook/ingress"
"github.com/clastix/capsule/pkg/webhook/namespace_quota"
"github.com/clastix/capsule/pkg/webhook/network_policies"
"github.com/clastix/capsule/pkg/webhook/owner_reference"
"github.com/clastix/capsule/pkg/webhook/pvc"
"github.com/clastix/capsule/version"
// +kubebuilder:scaffold:imports
)
var (
scheme = runtime.NewScheme()
setupLog = ctrl.Log.WithName("setup")
)
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(capsulev1alpha1.AddToScheme(scheme))
utilruntime.Must(corev1.AddToScheme(scheme))
// +kubebuilder:scaffold:scheme
}
func printVersion() {
setupLog.Info(fmt.Sprintf("Operator Version: %s", version.Version))
setupLog.Info(fmt.Sprintf("Go Version: %s", goRuntime.Version()))
setupLog.Info(fmt.Sprintf("Go OS/Arch: %s/%s", goRuntime.GOOS, goRuntime.GOARCH))
}
func main() {
var metricsAddr string
var enableLeaderElection bool
var v bool
flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
flag.BoolVar(&enableLeaderElection, "enable-leader-election", false,
"Enable leader election for controller manager. "+
"Enabling this will ensure there is only one active controller manager.")
flag.BoolVar(&v, "version", false, "Print the Capsule version and exit")
flag.Parse()
ctrl.SetLogger(zap.New(zap.UseDevMode(true)))
printVersion()
if v {
os.Exit(0)
}
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
MetricsBindAddress: metricsAddr,
Port: 9443,
LeaderElection: enableLeaderElection,
LeaderElectionID: "42c733ea.clastix.capsule.io",
})
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
}
if err = (&controllers.TenantReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("Tenant"),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Tenant")
os.Exit(1)
}
// +kubebuilder:scaffold:builder
//webhooks
err = webhook.Register(mgr, &ingress.ExtensionIngress{}, &ingress.NetworkIngress{}, pvc.Webhook{}, &owner_reference.Webhook{}, &namespace_quota.Webhook{}, network_policies.Webhook{})
if err != nil {
setupLog.Error(err, "unable to setup webhooks")
os.Exit(1)
}
if err = (&controllers.NamespaceReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("Namespace"),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Namespace")
os.Exit(1)
}
if err = (&secret.CaReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("CA"),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Namespace")
os.Exit(1)
}
if err = (&secret.TlsReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("Tls"),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Namespace")
os.Exit(1)
}
if err := indexer.AddToManager(mgr); err != nil {
setupLog.Error(err, "unable to setup indexers")
os.Exit(1)
}
setupLog.Info("starting manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
}
}

View File

@@ -1,23 +0,0 @@
/*
Copyright 2020 Clastix Labs.
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 apis
import (
"github.com/clastix/capsule/pkg/apis/capsule/v1alpha1"
)
func init() {
// Register the types with the Scheme so the components can map objects to GroupVersionKinds and back
AddToSchemes = append(AddToSchemes, v1alpha1.SchemeBuilder.AddToScheme)
}

View File

@@ -1,19 +0,0 @@
/*
Copyright 2020 Clastix Labs.
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 capsule contains capsule API versions.
//
// This file ensures Go source parsers acknowledge the capsule package
// and any child packages. It can be removed if any other Go source files are
// added to this package.
package capsule

View File

@@ -1,17 +0,0 @@
/*
Copyright 2020 Clastix Labs.
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 v1alpha1 contains API Schema definitions for the capsule v1alpha1 API group
// +k8s:deepcopy-gen=package,register
// +groupName=capsule.clastix.io
package v1alpha1

View File

@@ -1,3 +1,19 @@
/*
Copyright 2020 Clastix Labs.
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 cert
import (

View File

@@ -36,7 +36,7 @@ func TestCapsuleCa_GenerateCertificate(t *testing.T) {
}
for name, c := range map[string]testCase{
"foo.tld": {[]string{"foo.tld"}},
"SAN": {[]string{"capsule.capsule-system.svc", "capsule.capsule-system.default.cluster"}},
"SAN": {[]string{"capsule-webhook-service.capsule-system.svc", "capsule-webhook-service.capsule-system.default.cluster"}},
} {
t.Run(name, func(t *testing.T) {
var ca *CapsuleCa

View File

@@ -1,3 +1,19 @@
/*
Copyright 2020 Clastix Labs.
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 cert
type CaNotYetValidError struct{}

View File

@@ -1,3 +1,19 @@
/*
Copyright 2020 Clastix Labs.
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 cert
import "time"

View File

@@ -1,21 +0,0 @@
/*
Copyright 2020 Clastix Labs.
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 controller
import "github.com/clastix/capsule/pkg/controller/namespace"
func init() {
// AddToManagerFuncs is a list of functions to create controllers and add them to a manager.
AddToManagerFuncs = append(AddToManagerFuncs, namespace.Add)
}

View File

@@ -1,23 +0,0 @@
/*
Copyright 2020 Clastix Labs.
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 controller
import (
"github.com/clastix/capsule/pkg/controller/secret"
)
func init() {
// AddToManagerFuncs is a list of functions to create controllers and add them to a manager.
AddToManagerFuncs = append(AddToManagerFuncs, secret.AddTls, secret.AddCa)
}

View File

@@ -1,23 +0,0 @@
/*
Copyright 2020 Clastix Labs.
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 controller
import (
"github.com/clastix/capsule/pkg/controller/tenant"
)
func init() {
// AddToManagerFuncs is a list of functions to create controllers and add them to a manager.
AddToManagerFuncs = append(AddToManagerFuncs, tenant.Add)
}

View File

@@ -1,31 +0,0 @@
/*
Copyright 2020 Clastix Labs.
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 controller
import (
"sigs.k8s.io/controller-runtime/pkg/manager"
)
// AddToManagerFuncs is a list of functions to add all Controllers to the Manager
var AddToManagerFuncs []func(manager.Manager) error
// AddToManager adds all Controllers to the Manager
func AddToManager(m manager.Manager) error {
for _, f := range AddToManagerFuncs {
if err := f(m); err != nil {
return err
}
}
return nil
}

View File

@@ -1,8 +0,0 @@
package secret
type MissingCaError struct {
}
func (MissingCaError) Error() string {
return "CA has not been created yet, please generate a new"
}

View File

@@ -1,80 +0,0 @@
/*
Copyright 2020 Clastix Labs.
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 secret
import (
"context"
"fmt"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/clastix/capsule/pkg/cert"
)
type secretReconciliationFunc func(reconciler *ReconcileSecret, request reconcile.Request) (reconcile.Result, error)
// ReconcileSecret reconciles a Secret object
type ReconcileSecret struct {
client client.Client
scheme *runtime.Scheme
logger logr.Logger
reconcileFunc secretReconciliationFunc
}
func newReconciler(mgr manager.Manager, name string, f secretReconciliationFunc) reconcile.Reconciler {
return &ReconcileSecret{
client: mgr.GetClient(),
scheme: mgr.GetScheme(),
logger: log.Log.WithName(name),
reconcileFunc: f,
}
}
func (r *ReconcileSecret) Reconcile(request reconcile.Request) (reconcile.Result, error) {
return r.reconcileFunc(r, request)
}
func (r *ReconcileSecret) GetCertificateAuthority() (ca cert.Ca, err error) {
instance := &corev1.Secret{}
err = r.client.Get(context.TODO(), types.NamespacedName{
Namespace: "capsule-system",
Name: CaSecretName,
}, instance)
if err != nil {
return nil, fmt.Errorf("missing secret %s, cannot reconcile", CaSecretName)
}
if instance.Data == nil {
return nil, MissingCaError{}
}
ca, err = cert.NewCertificateAuthorityFromBytes(instance.Data[Cert], instance.Data[PrivateKey])
if err != nil {
return
}
return
}
func filterByName(objName, desired string) bool {
return objName == desired
}

View File

@@ -1,180 +0,0 @@
/*
Copyright 2020 Clastix Labs.
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 secret
import (
"bytes"
"context"
"errors"
"time"
v1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
"github.com/clastix/capsule/pkg/cert"
)
// Add creates a new Secret Controller and adds it to the Manager. The Manager will set fields on the Controller
// and Start it when the Manager is Started.
func AddCa(mgr manager.Manager) error {
r := newReconciler(mgr, "controller_secret", caReconcile)
return ca(mgr, r)
}
// add adds a new Controller to mgr with r as the reconcile.Reconciler
func ca(mgr manager.Manager, r reconcile.Reconciler) error {
// Create a new controller
c, err := controller.New("secret-controller", mgr, controller.Options{Reconciler: r})
if err != nil {
return err
}
// Watch for changes to CA Secret
err = c.Watch(&source.Kind{Type: &corev1.Secret{}}, &handler.EnqueueRequestForObject{}, predicate.Funcs{
CreateFunc: func(event event.CreateEvent) (ok bool) {
return filterByName(event.Meta.GetName(), CaSecretName)
},
DeleteFunc: func(deleteEvent event.DeleteEvent) bool {
return filterByName(deleteEvent.Meta.GetName(), CaSecretName)
},
UpdateFunc: func(updateEvent event.UpdateEvent) bool {
return filterByName(updateEvent.MetaNew.GetName(), CaSecretName)
},
GenericFunc: func(genericEvent event.GenericEvent) bool {
return filterByName(genericEvent.Meta.GetName(), CaSecretName)
},
})
if err != nil {
return err
}
return nil
}
func caReconcile(r *ReconcileSecret, request reconcile.Request) (reconcile.Result, error) {
var err error
r.logger = r.logger.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
r.logger.Info("Reconciling CA Secret")
// Fetch the CA instance
instance := &corev1.Secret{}
err = r.client.Get(context.TODO(), request.NamespacedName, instance)
if err != nil {
// Error reading the object - requeue the request.
return reconcile.Result{}, err
}
var ca cert.Ca
var rq time.Duration
ca, err = r.GetCertificateAuthority()
if err != nil && errors.Is(err, MissingCaError{}) {
ca, err = cert.GenerateCertificateAuthority()
if err != nil {
return reconcile.Result{}, err
}
} else if err != nil {
return reconcile.Result{}, err
}
r.logger.Info("Handling CA Secret")
rq, err = ca.ExpiresIn(time.Now())
if err != nil {
r.logger.Info("CA is expired, cleaning to obtain a new one")
instance.Data = map[string][]byte{}
} else {
r.logger.Info("Updating CA secret with new PEM and RSA")
var crt *bytes.Buffer
var key *bytes.Buffer
crt, _ = ca.CaCertificatePem()
key, _ = ca.CaPrivateKeyPem()
instance.Data = map[string][]byte{
Cert: crt.Bytes(),
PrivateKey: key.Bytes(),
}
wh := &v1.MutatingWebhookConfiguration{}
err = r.client.Get(context.TODO(), types.NamespacedName{
Name: "capsule",
}, wh)
if err != nil {
r.logger.Error(err, "cannot retrieve MutatingWebhookConfiguration")
return reconcile.Result{}, err
}
for i, w := range wh.Webhooks {
// Updating CABundle only in case of an internal service reference
if w.ClientConfig.Service != nil {
wh.Webhooks[i].ClientConfig.CABundle = instance.Data[Cert]
}
}
err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
return r.client.Update(context.TODO(), wh, &client.UpdateOptions{})
})
if err != nil {
r.logger.Error(err, "cannot update MutatingWebhookConfiguration webhooks CA bundle")
return reconcile.Result{}, err
}
}
var res controllerutil.OperationResult
t := &corev1.Secret{ObjectMeta: instance.ObjectMeta}
res, err = controllerutil.CreateOrUpdate(context.TODO(), r.client, t, func() error {
t.Data = instance.Data
return nil
})
if err != nil {
r.logger.Error(err, "cannot update Capsule TLS")
return reconcile.Result{}, err
}
if res == controllerutil.OperationResultUpdated {
r.logger.Info("Capsule CA has been updated, we need to trigger TLS update too")
tls := &corev1.Secret{}
err = r.client.Get(context.TODO(), types.NamespacedName{
Namespace: "capsule-system",
Name: TlsSecretName,
}, tls)
if err != nil {
r.logger.Error(err, "Capsule TLS Secret missing")
}
err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
_, err = controllerutil.CreateOrUpdate(context.TODO(), r.client, tls, func() error {
tls.Data = map[string][]byte{}
return nil
})
return err
})
if err != nil {
r.logger.Error(err, "Cannot clean Capsule TLS Secret due to CA update")
return reconcile.Result{}, err
}
}
r.logger.Info("Reconciliation completed, processing back in " + rq.String())
return reconcile.Result{Requeue: true, RequeueAfter: rq}, nil
}

View File

@@ -1,150 +0,0 @@
/*
Copyright 2020 Clastix Labs.
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 secret
import (
"context"
"crypto/x509"
"encoding/pem"
"os"
"time"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
"github.com/clastix/capsule/pkg/cert"
)
// Add creates a new Secret Controller and adds it to the Manager. The Manager will set fields on the Controller
// and Start it when the Manager is Started.
func AddTls(mgr manager.Manager) error {
return tls(mgr, newReconciler(mgr, "controller_secret_tls", tlsReconcile))
}
// add adds a new Controller to mgr with r as the reconcile.Reconciler
func tls(mgr manager.Manager, r reconcile.Reconciler) error {
// Create a new controller
c, err := controller.New("secret-controller", mgr, controller.Options{Reconciler: r})
if err != nil {
return err
}
// Watch for changes to TLS Secret
err = c.Watch(&source.Kind{Type: &corev1.Secret{}}, &handler.EnqueueRequestForObject{}, predicate.Funcs{
CreateFunc: func(event event.CreateEvent) (ok bool) {
return filterByName(event.Meta.GetName(), TlsSecretName)
},
DeleteFunc: func(deleteEvent event.DeleteEvent) bool {
return filterByName(deleteEvent.Meta.GetName(), TlsSecretName)
},
UpdateFunc: func(updateEvent event.UpdateEvent) bool {
return filterByName(updateEvent.MetaNew.GetName(), TlsSecretName)
},
GenericFunc: func(genericEvent event.GenericEvent) bool {
return filterByName(genericEvent.Meta.GetName(), TlsSecretName)
},
})
if err != nil {
return err
}
return nil
}
func tlsReconcile(r *ReconcileSecret, request reconcile.Request) (reconcile.Result, error) {
var err error
r.logger = r.logger.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
r.logger.Info("Reconciling TLS/CA Secret")
// Fetch the Secret instance
instance := &corev1.Secret{}
err = r.client.Get(context.TODO(), request.NamespacedName, instance)
if err != nil {
// Error reading the object - requeue the request.
return reconcile.Result{}, err
}
var ca cert.Ca
var rq time.Duration
ca, err = r.GetCertificateAuthority()
if err != nil {
return reconcile.Result{}, err
}
var shouldCreate bool
for _, key := range []string{Cert, PrivateKey} {
if _, ok := instance.Data[key]; !ok {
shouldCreate = true
}
}
if shouldCreate {
r.logger.Info("Missing Capsule TLS certificate")
rq = 6 * 30 * 24 * time.Hour
opts := cert.NewCertOpts(time.Now().Add(rq), "capsule.capsule-system.svc")
crt, key, err := ca.GenerateCertificate(opts)
if err != nil {
r.logger.Error(err, "Cannot generate new TLS certificate")
return reconcile.Result{}, err
}
instance.Data = map[string][]byte{
Cert: crt.Bytes(),
PrivateKey: key.Bytes(),
}
} else {
var c *x509.Certificate
var b *pem.Block
b, _ = pem.Decode(instance.Data[Cert])
c, err = x509.ParseCertificate(b.Bytes)
if err != nil {
r.logger.Error(err, "cannot parse Capsule TLS")
return reconcile.Result{}, err
}
rq = time.Duration(c.NotAfter.Unix()-time.Now().Unix()) * time.Second
if time.Now().After(c.NotAfter) {
r.logger.Info("Capsule TLS is expired, cleaning to obtain a new one")
instance.Data = map[string][]byte{}
}
}
var res controllerutil.OperationResult
t := &corev1.Secret{ObjectMeta: instance.ObjectMeta}
res, err = controllerutil.CreateOrUpdate(context.TODO(), r.client, t, func() error {
t.Data = instance.Data
return nil
})
if err != nil {
r.logger.Error(err, "cannot update Capsule TLS")
return reconcile.Result{}, err
}
if instance.Name == TlsSecretName && res == controllerutil.OperationResultUpdated {
r.logger.Info("Capsule TLS certificates has been updated, we need to restart the Controller")
os.Exit(15)
}
r.logger.Info("Reconciliation completed, processing back in " + rq.String())
return reconcile.Result{Requeue: true, RequeueAfter: rq}, nil
}

View File

@@ -1,9 +1,12 @@
/*
Copyright 2020 Clastix Labs.
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.

View File

@@ -1,9 +1,12 @@
/*
Copyright 2020 Clastix Labs.
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.

View File

@@ -1,9 +1,12 @@
/*
Copyright 2020 Clastix Labs.
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.

View File

@@ -1,9 +1,12 @@
/*
Copyright 2020 Clastix Labs.
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.
@@ -17,7 +20,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/clastix/capsule/pkg/apis/capsule/v1alpha1"
"github.com/clastix/capsule/api/v1alpha1"
)
type NamespacesReference struct {

View File

@@ -1,9 +1,12 @@
/*
Copyright 2020 Clastix Labs.
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.
@@ -17,7 +20,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/clastix/capsule/pkg/apis/capsule/v1alpha1"
"github.com/clastix/capsule/api/v1alpha1"
)
type OwnerReference struct {

View File

@@ -1,9 +1,12 @@
/*
Copyright 2020 Clastix Labs.
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.
@@ -17,7 +20,7 @@ import (
"sort"
"strings"
"github.com/clastix/capsule/pkg/apis/capsule/v1alpha1"
"github.com/clastix/capsule/api/v1alpha1"
)
type UserGroupList []string
@@ -35,7 +38,7 @@ func (u UserGroupList) Swap(i, j int) {
}
func (u UserGroupList) IsInCapsuleGroup() (ok bool) {
v := v1alpha1.SchemeGroupVersion.Group
v := v1alpha1.GroupVersion.Group
sort.Sort(u)
i := sort.SearchStrings(u, v)

View File

@@ -1,22 +0,0 @@
/*
Copyright 2020 Clastix Labs.
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 webhook
import (
"github.com/clastix/capsule/pkg/webhook/namespace_quota"
)
func init() {
AddToWebhookServer = append(AddToWebhookServer, namespace_quota.Add)
}

View File

@@ -1,9 +1,12 @@
/*
Copyright 2020 Clastix Labs.
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.
@@ -14,9 +17,14 @@ limitations under the License.
package webhook
import (
"github.com/clastix/capsule/pkg/webhook/ingress_class"
"context"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)
func init() {
AddToWebhookServer = append(AddToWebhookServer, ingress_class.AddExtensions, ingress_class.AddNetworking)
type Handler interface {
OnCreate(ctx context.Context, req admission.Request, client client.Client, decoder *admission.Decoder) admission.Response
OnDelete(ctx context.Context, req admission.Request, client client.Client, decoder *admission.Decoder) admission.Response
OnUpdate(ctx context.Context, req admission.Request, client client.Client, decoder *admission.Decoder) admission.Response
}

View File

@@ -1,9 +1,12 @@
/*
Copyright 2020 Clastix Labs.
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.
@@ -11,16 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package apis
package ingress
import (
"k8s.io/apimachinery/pkg/runtime"
"fmt"
)
// AddToSchemes may be used to add all resources defined in the project to a Scheme
var AddToSchemes runtime.SchemeBuilder
// AddToScheme adds all Resources to the Scheme
func AddToScheme(s *runtime.Scheme) error {
return AddToSchemes.AddToScheme(s)
type ingressClassForbidden struct {
ingressClass string
}
func NewIngressClassForbidden(ingressClass string) error {
return &ingressClassForbidden{ingressClass: ingressClass}
}
func (i ingressClassForbidden) Error() string {
return fmt.Sprintf("Ingress Class %s is forbidden for the current Tenant", i.ingressClass)
}

View File

@@ -0,0 +1,69 @@
/*
Copyright 2020 Clastix Labs.
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 ingress
import (
"context"
"net/http"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"github.com/clastix/capsule/pkg/webhook"
)
// +kubebuilder:webhook:path=/validating-v1-extensions-ingress,mutating=false,failurePolicy=fail,groups=extensions,resources=ingresses,verbs=create;update,versions=v1beta1,name=extensions.ingress.capsule.clastix.io
type ExtensionIngress struct{}
func (r *ExtensionIngress) GetHandler() webhook.Handler {
return &extensionIngressHandler{}
}
func (r *ExtensionIngress) GetName() string {
return "ExtensionIngress"
}
func (r *ExtensionIngress) GetPath() string {
return "/validating-v1-extensions-ingress"
}
type extensionIngressHandler struct {
}
func (r *extensionIngressHandler) OnCreate(ctx context.Context, req admission.Request, client client.Client, decoder *admission.Decoder) admission.Response {
i := &extensionsv1beta1.Ingress{}
if err := decoder.Decode(req, i); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
return handleIngress(ctx, i, i.Spec.IngressClassName, client)
}
func (r *extensionIngressHandler) OnDelete(ctx context.Context, req admission.Request, client client.Client, decoder *admission.Decoder) admission.Response {
return admission.Allowed("")
}
func (r *extensionIngressHandler) OnUpdate(ctx context.Context, req admission.Request, client client.Client, decoder *admission.Decoder) admission.Response {
i := &extensionsv1beta1.Ingress{}
if err := decoder.Decode(req, i); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
return handleIngress(ctx, i, i.Spec.IngressClassName, client)
}

View File

@@ -1,9 +1,12 @@
/*
Copyright 2020 Clastix Labs.
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.
@@ -11,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package ingress_class
package ingress
import (
"context"
@@ -23,7 +26,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"github.com/clastix/capsule/pkg/apis/capsule/v1alpha1"
"github.com/clastix/capsule/api/v1alpha1"
)
func handleIngress(ctx context.Context, object metav1.Object, ic *string, c client.Client) admission.Response {
@@ -43,8 +46,7 @@ func handleIngress(ctx context.Context, object metav1.Object, ic *string, c clie
}
if !tl.Items[0].Spec.IngressClasses.IsStringInList(*ic) {
err := fmt.Errorf("Ingress Class %s is forbidden for the current Tenant", *ic)
return admission.Errored(http.StatusBadRequest, err)
return admission.Errored(http.StatusBadRequest, NewIngressClassForbidden(*ic))
}
return admission.Allowed("")

View File

@@ -0,0 +1,67 @@
/*
Copyright 2020 Clastix Labs.
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 ingress
import (
"context"
"net/http"
networkingv1beta1 "k8s.io/api/networking/v1beta1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"github.com/clastix/capsule/pkg/webhook"
)
// +kubebuilder:webhook:path=/validating-v1-networking-ingress,mutating=false,failurePolicy=fail,groups=networking.k8s.io,resources=ingresses,verbs=create;update,versions=v1beta1,name=networking.ingress.capsule.clastix.io
type NetworkIngress struct{}
func (r *NetworkIngress) GetHandler() webhook.Handler {
return &handler{}
}
func (r *NetworkIngress) GetName() string {
return "NetworkIngress"
}
func (r *NetworkIngress) GetPath() string {
return "/validating-v1-networking-ingress"
}
type handler struct {
}
func (r *handler) OnCreate(ctx context.Context, req admission.Request, client client.Client, decoder *admission.Decoder) admission.Response {
i := &networkingv1beta1.Ingress{}
if err := decoder.Decode(req, i); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
return handleIngress(ctx, i, i.Spec.IngressClassName, client)
}
func (r *handler) OnDelete(ctx context.Context, req admission.Request, client client.Client, decoder *admission.Decoder) admission.Response {
return admission.Allowed("")
}
func (r *handler) OnUpdate(ctx context.Context, req admission.Request, client client.Client, decoder *admission.Decoder) admission.Response {
i := &networkingv1beta1.Ingress{}
if err := decoder.Decode(req, i); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
return handleIngress(ctx, i, i.Spec.IngressClassName, client)
}

View File

@@ -1,64 +0,0 @@
/*
Copyright 2020 Clastix Labs.
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 ingress_class
import (
"context"
"net/http"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"github.com/clastix/capsule/pkg/webhook/utils"
)
func AddExtensions(mgr manager.Manager) error {
mgr.GetWebhookServer().Register("/validating-v1-extensions-ingress", &webhook.Admission{
Handler: &extensionIngress{},
})
return nil
}
type extensionIngress struct {
client client.Client
decoder *admission.Decoder
}
func (r *extensionIngress) Handle(ctx context.Context, req admission.Request) admission.Response {
g := utils.UserGroupList(req.UserInfo.Groups)
if !g.IsInCapsuleGroup() {
// not a Capsule user, can be skipped
return admission.Allowed("")
}
i := &extensionsv1beta1.Ingress{}
if err := r.decoder.Decode(req, i); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
return handleIngress(ctx, i, i.Spec.IngressClassName, r.client)
}
func (r *extensionIngress) InjectDecoder(d *admission.Decoder) error {
r.decoder = d
return nil
}
func (r *extensionIngress) InjectClient(c client.Client) error {
r.client = c
return nil
}

View File

@@ -1,64 +0,0 @@
/*
Copyright 2020 Clastix Labs.
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 ingress_class
import (
"context"
"net/http"
networkingv1beta1 "k8s.io/api/networking/v1beta1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"github.com/clastix/capsule/pkg/webhook/utils"
)
func AddNetworking(mgr manager.Manager) error {
mgr.GetWebhookServer().Register("/validating-v1-networking-ingress", &webhook.Admission{
Handler: &validatingV1{},
})
return nil
}
type validatingV1 struct {
client client.Client
decoder *admission.Decoder
}
func (r *validatingV1) Handle(ctx context.Context, req admission.Request) admission.Response {
g := utils.UserGroupList(req.UserInfo.Groups)
if !g.IsInCapsuleGroup() {
// not a Capsule user, can be skipped
return admission.Allowed("")
}
i := &networkingv1beta1.Ingress{}
if err := r.decoder.Decode(req, i); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
return handleIngress(ctx, i, i.Spec.IngressClassName, r.client)
}
func (r *validatingV1) InjectDecoder(d *admission.Decoder) error {
r.decoder = d
return nil
}
func (r *validatingV1) InjectClient(c client.Client) error {
r.client = c
return nil
}

Some files were not shown because too many files have changed in this diff Show More