Compare commits

..

6 Commits

Author SHA1 Message Date
Massimiliano Giovagnoli
857c338c53 wip
Signed-off-by: Massimiliano Giovagnoli <me@maxgio.it>
2022-08-15 20:51:45 +02:00
Massimiliano Giovagnoli
5a9c25b125 refactor(api/v1beta1/owner_role.go): split cluster role that need to be cluster bound
Signed-off-by: Massimiliano Giovagnoli <me@maxgio.it>
2022-08-13 18:56:35 +02:00
Massimiliano Giovagnoli
3cd7bfe6d4 chore(controllers/tenant): rename tenant clusterrole controller
Signed-off-by: Massimiliano Giovagnoli <me@maxgio.it>
2022-08-13 18:29:09 +02:00
Massimiliano Giovagnoli
ff53cc2f38 feat(controllers/tenant): ensure per-tenant owners roles
add gitops ready cluster roles per tenant owners.

Signed-off-by: Massimiliano Giovagnoli <me@maxgio.it>
2022-08-13 16:10:48 +02:00
Massimiliano Giovagnoli
852ab16323 feat(api/v1beta1/owner_role): bind gitops roles to owners
Signed-off-by: Massimiliano Giovagnoli <me@maxgio.it>
2022-08-13 16:00:04 +02:00
Massimiliano Giovagnoli
9c18471879 feat(tenant/tenant/spec): add initial knob to enable the gitops-ready rbac
Signed-off-by: Massimiliano Giovagnoli <me@maxgio.it>
2022-08-13 15:50:25 +02:00
366 changed files with 6080 additions and 28526 deletions

View File

@@ -9,7 +9,10 @@ assignees: ''
<!--
Thanks for taking time reporting a Capsule bug!
We do our best to keep it reliable and working, so don't hesitate adding
as many information as you can and keep in mind you can reach us on our
Clastix Slack workspace: https://clastix.slack.com, #capsule channel.
-->
# Bug description

View File

@@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Chat on Slack
url: https://kubernetes.slack.com/archives/C03GETTJQRL
about: Maybe chatting with the community can help

View File

@@ -14,6 +14,8 @@ We're trying to build a community drive Open Source project, so don't
hesitate proposing your enhancement ideas: keep in mind, since we would like
to keep it as agnostic as possible, to motivate all your assumptions.
If you need to reach the maintainers, please join the Clastix Slack workspace:
https://clastix.slack.com, #capsule channel.
-->
# Describe the feature

View File

@@ -15,4 +15,6 @@ following ourselves these points:
- explain what and why in the body, if more than a trivial change, wrapping at
72 characters
If you have any issue or question, reach out us!
https://clastix.slack.com >>> #capsule channel
-->

View File

@@ -1,21 +0,0 @@
name: Checks if an input is defined
description: Checks if an input is defined and outputs 'true' or 'false'.
inputs:
value:
description: value to test
required: true
outputs:
result:
description: outputs 'true' or 'false' if input value is defined or not
value: ${{ steps.check.outputs.result }}
runs:
using: composite
steps:
- shell: bash
id: check
run: |
echo "result=${{ inputs.value != '' }}" >> $GITHUB_OUTPUT

View File

@@ -1,20 +0,0 @@
name: Setup caches
description: Setup caches for go modules and build cache.
inputs:
build-cache-key:
description: build cache prefix
runs:
using: composite
steps:
- uses: actions/cache@4723a57e26efda3a62cbde1812113b730952852d # v3.2.2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-pkg-mod-${{ hashFiles('**/go.sum') }}-${{ hashFiles('Makefile') }}
- uses: actions/cache@4723a57e26efda3a62cbde1812113b730952852d # v3.2.2
if: ${{ inputs.build-cache-key }}
with:
path: ~/.cache/go-build
key: ${{ runner.os }}-build-cache-${{ inputs.build-cache-key }}-${{ hashFiles('**/go.sum') }}-${{ hashFiles('Makefile') }}

View File

@@ -1,10 +0,0 @@
remote: origin
target-branch: main
chart-dirs:
- charts
helm-extra-args: "--timeout 600s"
validate-chart-schema: false
validate-maintainers: false
validate-yaml: true
exclude-deprecated: true
check-version-increment: false

View File

@@ -1,43 +0,0 @@
---
rules:
braces:
min-spaces-inside: 0
max-spaces-inside: 0
min-spaces-inside-empty: -1
max-spaces-inside-empty: -1
brackets:
min-spaces-inside: 0
max-spaces-inside: 0
min-spaces-inside-empty: -1
max-spaces-inside-empty: -1
colons:
max-spaces-before: 0
max-spaces-after: 1
commas:
max-spaces-before: 0
min-spaces-after: 1
max-spaces-after: 1
comments:
require-starting-space: true
min-spaces-from-content: 1
document-end: disable
document-start: disable # No --- to start a file
empty-lines:
max: 2
max-start: 0
max-end: 0
hyphens:
max-spaces-after: 1
indentation:
spaces: consistent
indent-sequences: whatever # - list indentation will handle both indentation and without
check-multi-line-strings: false
key-duplicates: enable
line-length: disable # Lines can be any length
new-line-at-end-of-file: enable
new-lines:
type: unix
trailing-spaces: enable
truthy:
level: warning

View File

@@ -1,16 +0,0 @@
version: 2
updates:
- package-ecosystem: gomod
directory: /
schedule:
interval: daily
rebase-strategy: disabled
commit-message:
prefix: "feat(deps)"
- package-ecosystem: github-actions
directory: /
schedule:
interval: daily
rebase-strategy: disabled
commit-message:
prefix: "ci"

View File

@@ -2,22 +2,17 @@
github: https://github.com/bsctl
company: Clastix
projects:
- https://github.com/projectcapsule/capsule
- https://github.com/clastix/capsule
- https://github.com/clastix/capsule-proxy
- name: Dario Tranchitella
github: https://github.com/prometherion
company: Clastix
projects:
- https://github.com/projectcapsule/capsule
- https://github.com/clastix/capsule
- https://github.com/clastix/capsule-proxy
- name: Maksim Fedotov
github: https://github.com/MaxFedotov
company: wargaming.net
projects:
- https://github.com/projectcapsule/capsule
- https://github.com/clastix/capsule
- https://github.com/clastix/capsule-proxy
- name: Oliver Bähler
github: https://github.com/oliverbaehler
company: Bedag Informatik AG
projects:
- https://github.com/projectcapsule/capsule

View File

@@ -1,24 +0,0 @@
name: Check actions
permissions: {}
on:
pull_request:
branches: [ "main" ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Ensure SHA pinned actions
uses: zgosalvez/github-actions-ensure-sha-pinned-actions@f32435541e24cd6a4700a7f52bb2ec59e80603b1 # v2.1.4
with:
# slsa-github-generator requires using a semver tag for reusable workflows.
# See: https://github.com/slsa-framework/slsa-github-generator#referencing-slsa-builders-and-generators
allowlist: |
slsa-framework/slsa-github-generator

View File

@@ -1,23 +0,0 @@
name: Check Commit
permissions: {}
on:
push:
branches: [ "*" ]
pull_request:
branches: [ "*" ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
commit_lint:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
fetch-depth: 0
- uses: wagoid/commitlint-github-action@6319f54d83768b60acd6fd60e61007ccc583e62f #v5.4.3
with:
firstParent: true

View File

@@ -1,5 +1,4 @@
name: Diff checks
permissions: {}
name: CI
on:
push:
@@ -7,27 +6,40 @@ on:
pull_request:
branches: [ "*" ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
diff:
name: diff
runs-on: ubuntu-20.04
commit_lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
- uses: wagoid/commitlint-github-action@v2
with:
go-version: '1.19'
firstParent: true
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v2.3.0
with:
version: v1.45.2
only-new-issues: false
args: --timeout 2m --config .golangci.yml
diff:
name: diff
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/setup-go@v2
with:
go-version: '1.18'
- run: make installer
- name: Checking if YAML installer file is not aligned
run: if [[ $(git diff | wc -l) -gt 0 ]]; then echo ">>> Untracked generated files have not been committed" && git --no-pager diff && exit 1; fi
- run: make apidoc
- name: Checking if the CRDs documentation is not aligned
run: if [[ $(git diff | wc -l) -gt 0 ]]; then echo ">>> CRDs generated documentation have not been committed" && git --no-pager diff && exit 1; fi
- name: Checking if YAML installer generated untracked files
run: test -z "$(git ls-files --others --exclude-standard 2> /dev/null)"
- name: Checking if source code is not formatted

View File

@@ -1,38 +0,0 @@
name: Codecov
permissions: {}
on:
pull_request:
branches: [ "main" ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
codecov:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Setup caches
uses: ./.github/actions/setup-caches
timeout-minutes: 5
continue-on-error: true
with:
build-cache-key: codecov
- name: Check secret
id: checksecret
uses: ./.github/actions/exists
with:
value: ${{ secrets.CODECOV_TOKEN }}
- name: Generate Code Coverage Report
if: steps.checksecret.outputs.result == 'true'
run: make test
- name: Upload Report to Codecov
if: steps.checksecret.outputs.result == 'true'
uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4
with:
file: ./coverage.out
fail_ci_if_error: true
verbose: true

97
.github/workflows/docker-ci.yml vendored Normal file
View File

@@ -0,0 +1,97 @@
name: docker-ci
on:
push:
tags:
- "v*"
jobs:
docker-ci:
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Generate build-args
id: build-args
run: |
# Declare vars for internal use
VERSION=$(git describe --abbrev=0 --tags)
GIT_HEAD_COMMIT=$(git rev-parse --short HEAD)
GIT_TAG_COMMIT=$(git rev-parse --short $VERSION)
GIT_MODIFIED_1=$(git diff $GIT_HEAD_COMMIT $GIT_TAG_COMMIT --quiet && echo "" || echo ".dev")
GIT_MODIFIED_2=$(git diff --quiet && echo "" || echo ".dirty")
# Export to GH_ENV
echo "GIT_LAST_TAG=$VERSION" >> $GITHUB_ENV
echo "GIT_HEAD_COMMIT=$GIT_HEAD_COMMIT" >> $GITHUB_ENV
echo "GIT_TAG_COMMIT=$GIT_TAG_COMMIT" >> $GITHUB_ENV
echo "GIT_MODIFIED=$(echo "$GIT_MODIFIED_1""$GIT_MODIFIED_2")" >> $GITHUB_ENV
echo "GIT_REPO=$(git config --get remote.origin.url)" >> $GITHUB_ENV
echo "BUILD_DATE=$(git log -1 --format="%at" | xargs -I{} date -d @{} +%Y-%m-%dT%H:%M:%S)" >> $GITHUB_ENV
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: |
quay.io/${{ github.repository }}
docker.io/${{ github.repository }}
tags: |
type=semver,pattern={{raw}}
flavor: |
latest=false
- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v1
with:
platforms: arm64,arm
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
with:
install: true
- name: Inspect builder
run: |
echo "Name: ${{ steps.buildx.outputs.name }}"
echo "Endpoint: ${{ steps.buildx.outputs.endpoint }}"
echo "Status: ${{ steps.buildx.outputs.status }}"
echo "Flags: ${{ steps.buildx.outputs.flags }}"
echo "Platforms: ${{ steps.buildx.outputs.platforms }}"
- name: Login to quay.io Container Registry
uses: docker/login-action@v1
with:
registry: quay.io
username: ${{ github.repository_owner }}+github
password: ${{ secrets.BOT_QUAY_IO }}
- name: Login to docker.io Container Registry
uses: docker/login-action@v1
with:
registry: docker.io
username: ${{ secrets.USER_DOCKER_IO }}
password: ${{ secrets.BOT_DOCKER_IO }}
- name: Build and push
id: build-release
uses: docker/build-push-action@v2
with:
file: Dockerfile
context: .
platforms: linux/amd64,linux/arm64,linux/arm
push: true
tags: ${{ steps.meta.outputs.tags }}
build-args: |
GIT_HEAD_COMMIT=${{ env.GIT_HEAD_COMMIT }}
GIT_TAG_COMMIT=${{ env.GIT_TAG_COMMIT }}
GIT_REPO=${{ env.GIT_REPO }}
GIT_LAST_TAG=${{ env.GIT_LAST_TAG }}
GIT_MODIFIED=${{ env.GIT_MODIFIED }}
BUILD_DATE=${{ env.BUILD_DATE }}
- name: Image digest
run: echo ${{ steps.build-release.outputs.digest }}

View File

@@ -1,69 +0,0 @@
name: Publish images
permissions: {}
on:
push:
tags:
- "v*"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
publish-images:
runs-on: ubuntu-latest
permissions:
packages: write
id-token: write
outputs:
capsule-digest: ${{ steps.publish-capsule.outputs.digest }}
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Setup caches
uses: ./.github/actions/setup-caches
timeout-minutes: 5
continue-on-error: true
with:
build-cache-key: publish-images
- name: Run Trivy vulnerability (Repo)
uses: aquasecurity/trivy-action@fbd16365eb88e12433951383f5e99bd901fc618f # v0.12.0
with:
scan-type: 'fs'
ignore-unfixed: true
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Install Cosign
uses: sigstore/cosign-installer@11086d25041f77fe8fe7b9ea4e48e3b9192b8f19 # v3.1.2
- name: Publish Capsule
id: publish-capsule
uses: oliverbaehler/github-actions/ko-publish-image@979018716f7d0cbe8d2711f572b350afad4ef211 # v0.1.1
with:
makefile-target: ko-publish-capsule
registry: ghcr.io
registry-username: ${{ github.actor }}
registry-password: ${{ secrets.GITHUB_TOKEN }}
repository: ${{ github.repository_owner }}
version: ${{ github.ref_name }}
sign-image: true
sbom-name: capsule
sbom-repository: ghcr.io/${{ github.repository_owner }}/sbom
signature-repository: ghcr.io/${{ github.repository_owner }}/signatures
main-path: ./
env:
REPOSITORY: ${{ github.repository }}
generate-capsule-provenance:
needs: publish-images
permissions:
id-token: write # To sign the provenance.
packages: write # To upload assets to release.
actions: read # To read the workflow path.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.9.0
with:
image: ghcr.io/${{ github.repository_owner }}/capsule
digest: "${{ needs.publish-images.outputs.capsule-digest }}"
registry-username: ${{ github.actor }}
secrets:
registry-password: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,31 +0,0 @@
name: docs-lint
permissions: {}
on:
push:
branches: [ "*" ]
paths:
- '.github/workflows/docs-lint.yml'
- 'docs/content/**'
pull_request:
branches: [ "*" ]
paths:
- '.github/workflows/docs-lint.yml'
- 'docs/content/**'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
spelling:
name: Spell Check
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
fetch-depth: 0
- uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
with:
node-version: 18
- run: make docs-lint

View File

@@ -1,5 +1,4 @@
name: e2e
permissions: {}
on:
push:
@@ -27,35 +26,31 @@ on:
- 'main.go'
- 'Makefile'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
kind:
name: Kubernetes
strategy:
fail-fast: false
matrix:
k8s-version: ['v1.20.7', 'v1.21.2', 'v1.22.4', 'v1.23.6', 'v1.24.7', 'v1.25.3', 'v1.26.3', 'v1.27.2']
runs-on: ubuntu-20.04
k8s-version: ['v1.16.15', 'v1.17.11', 'v1.18.8', 'v1.19.4', 'v1.20.7', 'v1.21.2', 'v1.22.4', 'v1.23.6', 'v1.24.1']
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
- uses: actions/setup-go@v2
with:
go-version: '1.19'
go-version: '1.18'
- run: make manifests
- name: Checking if manifests are disaligned
run: test -z "$(git diff 2> /dev/null)"
- name: Checking if manifests generated untracked files
run: test -z "$(git ls-files --others --exclude-standard 2> /dev/null)"
- uses: engineerd/setup-kind@aa272fe2a7309878ffc2a81c56cfe3ef108ae7d0 # v0.5.0
- uses: engineerd/setup-kind@v0.5.0
with:
skipClusterCreation: true
version: v0.14.0
- uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3
- uses: azure/setup-helm@v1
with:
version: 3.3.4
- name: e2e testing

View File

@@ -1,35 +0,0 @@
name: FOSSA
permissions: {}
on:
push:
branches: [ "*" ]
pull_request:
branches: [ "*" ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
fossa-scan:
runs-on: ubuntu-20.04
steps:
- name: "Checkout Code"
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Check secret
id: checksecret
uses: ./.github/actions/exists
with:
value: ${{ secrets.FOSSA_API_KEY }}
- name: "Run FOSSA Scan"
if: steps.checksecret.outputs.result == 'true'
uses: fossas/fossa-action@f61a4c0c263690f2ddb54b9822a719c25a7b608f # v1.3.1
with:
api-key: ${{ secrets.FOSSA_API_KEY }}
- name: "Run FOSSA Test"
if: steps.checksecret.outputs.result == 'true'
uses: fossas/fossa-action@f61a4c0c263690f2ddb54b9822a719c25a7b608f # v1.3.1
with:
api-key: ${{ secrets.FOSSA_API_KEY }}
run-tests: true

View File

@@ -1,24 +1,18 @@
name: CI gosec
permissions: {}
on:
push:
branches: [ "*" ]
pull_request:
branches: [ "*" ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
tests:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
env:
GO111MODULE: on
steps:
- name: Checkout Source
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@v2
- name: Run Gosec Security Scanner
uses: securego/gosec@0ec6cd95d7bf02aef4ec2786e884868e0044875b # v2.18.1
uses: securego/gosec@master
with:
args: ./...

View File

@@ -1,64 +0,0 @@
name: Publish charts
permissions: read-all
on:
push:
tags: [ "helm-v*" ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
publish-helm:
# Skip this Release on forks
if: github.repository_owner == 'projectcapsule'
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Publish Helm chart
uses: stefanprodan/helm-gh-pages@0ad2bb377311d61ac04ad9eb6f252fb68e207260 # v1.7.0
with:
token: "${{ secrets.HELM_CHARTS_PUSH_TOKEN }}"
linting: off
charts_dir: charts
charts_url: https://${{ github.repository_owner }}.github.io/charts
owner: ${{ github.repository_owner }}
repository: charts
branch: gh-pages
commit_username: ${{ github.actor }}
publish-helm-oci:
runs-on: ubuntu-20.04
permissions:
contents: write
id-token: write
packages: write
outputs:
chart-digest: ${{ steps.helm_publish.outputs.digest }}
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: sigstore/cosign-installer@11086d25041f77fe8fe7b9ea4e48e3b9192b8f19 # v3.1.2
- name: Helm | Publish
id: helm_publish
uses: oliverbaehler/github-actions/helm-oci-chart@8dfd42735c85f6c58d5d4d6f3232cd0e39d1fe73 # v0.1.0
with:
registry: ghcr.io
repository: ${{ github.repository_owner }}/charts
name: "capsule"
registry-username: ${{ github.actor }}
registry-password: ${{ secrets.GITHUB_TOKEN }}
update-dependencies: 'true' # Defaults to false
sign-image: 'true'
signature-repository: ghcr.io/${{ github.repository_owner }}/signatures
helm-provenance:
needs: publish-helm-oci
permissions:
id-token: write # To sign the provenance.
packages: write # To upload assets to release.
actions: read # To read the workflow path.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.9.0
with:
image: ghcr.io/${{ github.repository_owner }}/charts/capsule
digest: "${{ needs.publish-helm-oci.outputs.chart-digest }}"
registry-username: ${{ github.actor }}
secrets:
registry-password: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,69 +0,0 @@
name: Test charts
permissions: {}
on:
pull_request:
branches: [ "main" ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
fetch-depth: 0
- uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3
- name: Linting Chart
run: helm lint ./charts/capsule
- name: Setup Chart Linting
id: lint
uses: helm/chart-testing-action@e8788873172cb653a90ca2e819d79d65a66d4e76 # v2.4.0
- name: Run chart-testing (list-changed)
id: list-changed
run: |
changed=$(ct list-changed --config ./.github/configs/ct.yaml)
if [[ -n "$changed" ]]; then
echo "::set-output name=changed::true"
fi
- name: Run chart-testing (lint)
run: ct lint --debug --config ./.github/configs/ct.yaml --lint-conf ./.github/configs/lintconf.yaml
- name: Run docs-testing (helm-docs)
id: helm-docs
run: |
make helm-docs
if [[ $(git diff --stat) != '' ]]; then
echo -e '\033[0;31mDocumentation outdated! (Run make helm-docs locally and commit)\033[0m ❌'
git diff --color
exit 1
else
echo -e '\033[0;32mDocumentation up to date\033[0m ✔'
fi
# ATTENTION: This is a workaround for the upcoming ApiVersion Conversions for the capsule CRDs
# With this workflow the current docker image is build and loaded into kind, otherwise the install fails
# In the future this must be removed and the chart-testing-action must be used
- name: Run chart-testing (install)
run: make helm-test
if: steps.list-changed.outputs.changed == 'true'
## Create KIND Cluster
- name: Create kind cluster
uses: helm/kind-action@dda0770415bac9fc20092cacbc54aa298604d140 # v1.8.0
if: steps.list-changed.outputs.changed == 'true'
# Install Required Operators/CRDs
- name: Prepare Cluster Operators/CRDs
run: |
# Cert-Manager CRDs
kubectl create -f https://github.com/cert-manager/cert-manager/releases/download/v1.9.1/cert-manager.crds.yaml
# Prometheus CRDs
kubectl create -f https://github.com/prometheus-operator/prometheus-operator/releases/download/v0.58.0/bundle.yaml
if: steps.list-changed.outputs.changed == 'true'
# Install Charts
- name: Run chart-testing (install)
run: ct install --debug --config ./.github/configs/ct.yaml
if: steps.list-changed.outputs.changed == 'true'

36
.github/workflows/helm.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: Helm Chart
on:
push:
branches: [ "*" ]
tags: [ "helm-v*" ]
pull_request:
branches: [ "*" ]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: azure/setup-helm@v1
with:
version: 3.3.4
- name: Linting Chart
run: helm lint ./charts/capsule
release:
if: startsWith(github.ref, 'refs/tags/helm-v')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Publish Helm chart
uses: stefanprodan/helm-gh-pages@master
with:
token: ${{ secrets.BOT_GITHUB_TOKEN }}
charts_dir: charts
charts_url: https://clastix.github.io/charts
owner: clastix
repository: charts
branch: gh-pages
target_dir: .
commit_username: prometherion
commit_email: dario@tranchitella.eu

View File

@@ -1,25 +0,0 @@
name: Linting
permissions: {}
on:
push:
branches: [ "*" ]
pull_request:
branches: [ "*" ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
golangci:
name: lint
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Run golangci-lint
uses: golangci/golangci-lint-action@3a919529898de77ec3da873e3063ca4b10e7f5cc # v3.7.0
with:
version: v1.51.2
only-new-issues: false
args: --timeout 5m --config .golangci.yml

View File

@@ -1,36 +0,0 @@
name: Go Release
permissions: {}
on:
push:
tags:
- 'v*'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
create-release:
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Setup caches
uses: ./.github/actions/setup-caches
timeout-minutes: 5
continue-on-error: true
- uses: creekorful/goreportcard-action@1f35ced8cdac2cba28c9a2f2288a16aacfd507f9 # v1.0
- uses: anchore/sbom-action/download-syft@78fc58e266e87a38d4194b2137a3d4e9bcaf7ca1
- name: Install Cosign
uses: sigstore/cosign-installer@11086d25041f77fe8fe7b9ea4e48e3b9192b8f19 # v3.1.2
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 # v5.0.0
with:
version: latest
args: release --clean --timeout 90m --debug
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,42 +0,0 @@
name: Scorecards supply-chain security
permissions: {}
on:
schedule:
- cron: '0 0 * * 5'
push:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
analysis:
runs-on: ubuntu-latest
permissions:
security-events: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
persist-credentials: false
- name: Run analysis
uses: ossf/scorecard-action@483ef80eb98fb506c348f7d62e28055e49fe2398 # v2.3.0
with:
results_file: results.sarif
results_format: sarif
repo_token: ${{ secrets.SCORECARD_READ_TOKEN }}
publish_results: true
- name: Upload artifact
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: SARIF file
path: results.sarif
retention-days: 5
- name: Upload to code-scanning
uses: github/codeql-action/upload-sarif@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4
with:
sarif_file: results.sarif

3
.gitignore vendored
View File

@@ -6,7 +6,6 @@
*.so
*.dylib
bin
dist/
# Test binary, build with `go test -c`
*.test
@@ -30,4 +29,4 @@ dist/
**/*.key
.DS_Store
*.tgz
kind.yaml

View File

@@ -14,12 +14,7 @@ linters-settings:
sections:
- standard
- default
- prefix(github.com/projectcapsule/capsule)
goheader:
template: |-
Copyright 2020-2023 Project Capsule Authors.
SPDX-License-Identifier: Apache-2.0
- prefix(github.com/clastix/capsule)
linters:
enable-all: true
disable:
@@ -39,16 +34,13 @@ linters:
- testpackage
- varnamelen
- wrapcheck
- exhaustruct
- varcheck
- structcheck
- nosnakecase
- deadcode
- ifshort
- nonamedreturns
issues:
exclude:
- Using the variable on range scope .* in function literal
service:
golangci-lint-version: 1.51.2
golangci-lint-version: 1.33.x
run:
skip-files:

View File

@@ -1,83 +0,0 @@
project_name: capsule
env:
- COSIGN_EXPERIMENTAL=true
- GO111MODULE=on
before:
hooks:
- go mod download
gomod:
proxy: false
builds:
- main: .
binary: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}"
env:
- CGO_ENABLED=0
goarch:
- amd64
- arm64
goos:
- linux
flags:
- -trimpath
mod_timestamp: '{{ .CommitTimestamp }}'
ldflags:
- >-
-X main.Version={{ .Tag }}
-X main.GitCommit={{ .Commit }}
-X main.GitTag={{ .Tag }}
-X main.GitTreeState={{ .Date }}
-X main.BuildDate={{ .Date }}
-X main.GitRepo={{ .ProjectName }}
release:
prerelease: auto
footer: |
Thanks to all the contributors!
**Full Changelog**: https://github.com/projectcapsule/{{ .ProjectName }}/compare/{{ .PreviousTag }}...{{ .Tag }}
**Docker Images**
- `ghcr.io/projectcapsule/{{ .ProjectName }}:{{ .Tag }}`
- `ghcr.io/projectcapsule/{{ .ProjectName }}:latest`
checksum:
name_template: 'checksums.txt'
changelog:
sort: asc
use: github
filters:
exclude:
- '^test:'
- '^chore'
- '^rebase:'
- 'merge conflict'
- Merge pull request
- Merge remote-tracking branch
- Merge branch
groups:
# https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional
- title: '🛠 Dependency updates'
regexp: '^.*?(feat|fix)\(deps\)!?:.+$'
order: 300
- title: '✨ New Features'
regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$'
order: 100
- title: '🐛 Bug fixes'
regexp: '^.*?fix(\([[:word:]]+\))??!?:.+$'
order: 200
- title: '📖 Documentation updates'
regexp: ^.*?docs(\([[:word:]]+\))??!?:.+$
order: 400
- title: '🚀 Build process updates'
regexp: ^.*?(build|ci)(\([[:word:]]+\))??!?:.+$
order: 400
- title: '📦 Other work'
order: 9999
sboms:
- artifacts: archive
signs:
- cmd: cosign
args:
- "sign-blob"
- "--output-signature=${signature}"
- "${artifact}"
- "--yes"
artifacts: all

View File

@@ -1,8 +0,0 @@
defaultPlatforms:
- linux/arm64
- linux/amd64
builds:
- id: capsule
main: ./
ldflags:
- '{{ if index .Env "LD_FLAGS" }}{{ .Env.LD_FLAGS }}{{ end }}'

View File

@@ -3,30 +3,3 @@
This is a list of companies that have adopted Capsule, feel free to open a Pull-Request to get yours listed.
## Adopters list (alphabetically)
### [Bedag Informatik AG](https://www.bedag.ch/)
![Bedag](https://www.bedag.ch/wGlobal/wGlobal/layout/images/logo.svg)
### [Fastweb](https://www.fastweb.it/)
![Fastweb](https://www.fastweb.it/grandi-aziende/gfx/common/logo-fastweb-header.svg)
### [Klarrio](https://klarrio.com/)
![Klarrio](https://klarrio.com/wp-content/uploads/klarrio.png)
### [PITS Global Data Recovery Services](https://www.pitsdatarecovery.net)
![PITS Global Data Recovery Services](https://www.pitsdatarecovery.net/wp-content/uploads/2020/09/pits-logo.svg)
### [Politecnico di Torino](https://www.polito.it/)
![Politecnico di Torino](https://www.polito.it/themes/custom/polito/logo.svg)
### [Reevo](https://www.reevo.it/)
![Reevo Cloud and CyberSecurity](https://www.dropbox.com/s/x3q6r0oqstgvtdr/Logo_ReeVo_270x200px.svg)
### [University of Torino](https://www.unito.it)
![University of Torino](https://www.unito.it/sites/all/themes/bsunito/img/logo_new_2022.svg)
### [Velocity](https://velocity.tech/)
![Velocity](https://raw.githubusercontent.com/yarelm/velocity-logo/main/velocity.png)
### [Wargaming.net](https://www.wargaming.net/)
![Wargaming.net](https://static-cspbe-eu.wargaming.net/images/logo@2x.png)

View File

@@ -1,130 +0,0 @@
# Contributor Covenant Code of Conduct
Capsule follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md).
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement by contacting
one of the [maintainers](https://raw.githubusercontent.com/clastix/capsule/master/.github/maintainers.yaml).
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md) and is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@@ -1,58 +0,0 @@
# Contributing
All contributions are welcome! If you find a bug or have a feature request, please open an issue or submit a pull request.
## Guidelines
## Pull Requests
## Commits
Commit messages should indicate the change and it's impact. The general format for commit messages is the following:
feat(ui): Add `Button` component
^ ^ ^
| | |__ Subject
| |_______ Scope
|____________ Type
The commits are checked on pull-request. If the commit message does not follow the format, the workflow will fail. See the [Types](#types) and [Scopes](#scopes) sections for more information.
## Types
The following types are allowed for commits and pull requests:
* `ci` or `build`: changes to buillding process/workflows
* `docs`: changes to documentation
* `feat`: new features
* `fix`: bug fixes
## Scopes
The following types are allowed for commits and pull requests:
* `all`: changes that affect all components
* `chart`: changes to the Helm chart
* `operator`: changes to the operator
* `docs`: changes to the documentation
* `website`: changes to the website
* `ci`: changes to the CI/CD workflows
* `build`: changes to the build process
* `test`: changes to the testing process
* `release`: changes to the release process
* `deps`: dependency updates
### Sign-Off
Developer Certificate of Origin (DCO) Sign off
For contributors to certify that they wrote or otherwise have the right to submit the code they are contributing to the project, we are requiring everyone to acknowledge this by signing their work which indicates you agree to the DCO found here.
To sign your work, just add a line like this at the end of your commit message:
Signed-off-by: Random J Developer <random@developer.example.org>
This can easily be done with the -s command line option to append this automatically to your commit message.
git commit -s -m 'This is my commit message'

View File

@@ -1,13 +1,5 @@
# Build the manager binary
FROM golang:1.19.10 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
FROM golang:1.18 as builder
ARG TARGETARCH
ARG GIT_HEAD_COMMIT
@@ -17,6 +9,14 @@ ARG GIT_MODIFIED
ARG GIT_REPO
ARG BUILD_DATE
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 version.go version.go

251
Makefile
View File

@@ -1,23 +1,8 @@
# Version
GIT_HEAD_COMMIT ?= $(shell git rev-parse --short HEAD)
VERSION ?= $(shell git describe --abbrev=0 --tags --match "v*")
ifndef VERSION
VERSION = $(GIT_HEAD_COMMIT)
endif
# Defaults
REGISTRY ?= ghcr.io
REPOSITORY ?= projectcapsule/capsule
GIT_TAG_COMMIT ?= $(shell git rev-parse --short $(VERSION))
GIT_MODIFIED_1 ?= $(shell git diff $(GIT_HEAD_COMMIT) $(GIT_TAG_COMMIT) --quiet && echo "" || echo ".dev")
GIT_MODIFIED_2 ?= $(shell git diff --quiet && echo "" || echo ".dirty")
GIT_MODIFIED ?= $(shell echo "$(GIT_MODIFIED_1)$(GIT_MODIFIED_2)")
GIT_REPO ?= $(shell git config --get remote.origin.url)
BUILD_DATE ?= $(shell git log -1 --format="%at" | xargs -I{} sh -c 'if [ "$(shell uname)" = "Darwin" ]; then date -r {} +%Y-%m-%dT%H:%M:%S; else date -d @{} +%Y-%m-%dT%H:%M:%S; fi')
IMG_BASE ?= $(REPOSITORY)
IMG ?= $(IMG_BASE):$(VERSION)
CAPSULE_IMG ?= $(REGISTRY)/$(IMG_BASE)
# Current Operator version
VERSION ?= $$(git describe --abbrev=0 --tags --match "v*")
# Default bundle image tag
BUNDLE_IMG ?= clastix/capsule:$(VERSION)-bundle
# Options for 'bundle-build'
ifneq ($(origin CHANNELS), undefined)
BUNDLE_CHANNELS := --channels=$(CHANNELS)
@@ -27,6 +12,11 @@ BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL)
endif
BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL)
# Image URL to use all building/pushing image targets
IMG ?= clastix/capsule:$(VERSION)
# Produce CRDs that work back to Kubernetes 1.11 (no version conversion)
CRD_OPTIONS ?= "crd:preserveUnknownFields=false"
# 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
@@ -34,16 +24,20 @@ else
GOBIN=$(shell go env GOBIN)
endif
# Get information about git current status
GIT_HEAD_COMMIT ?= $$(git rev-parse --short HEAD)
GIT_TAG_COMMIT ?= $$(git rev-parse --short $(VERSION))
GIT_MODIFIED_1 ?= $$(git diff $(GIT_HEAD_COMMIT) $(GIT_TAG_COMMIT) --quiet && echo "" || echo ".dev")
GIT_MODIFIED_2 ?= $$(git diff --quiet && echo "" || echo ".dirty")
GIT_MODIFIED ?= $$(echo "$(GIT_MODIFIED_1)$(GIT_MODIFIED_2)")
GIT_REPO ?= $$(git config --get remote.origin.url)
BUILD_DATE ?= $$(git log -1 --format="%at" | xargs -I{} date -d @{} +%Y-%m-%dT%H:%M:%S)
all: manager
# Run tests
.PHONY: test
test: test-clean generate manifests test-clean
@GO111MODULE=on go test -v ./... -coverprofile coverage.out
.PHONY: test-clean
test-clean: ## Clean tests cache
@go clean -testcache
test: generate manifests
go test ./... -coverprofile cover.out
# Build manager binary
manager: generate golint
@@ -55,7 +49,7 @@ run: generate manifests
# Creates the single file to install Capsule without any external dependency
installer: manifests kustomize
cd config/manager && $(KUSTOMIZE) edit set image controller=${CAPSULE_IMG}
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
$(KUSTOMIZE) build config/default > config/install.yaml
# Install CRDs into a cluster
@@ -78,39 +72,12 @@ remove: installer
# Generate manifests e.g. CRD, RBAC etc.
manifests: controller-gen
$(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
$(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
# Generate code
generate: controller-gen
$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."
apidoc: apidocs-gen
$(APIDOCS_GEN) crdoc --resources config/crd/bases --output docs/content/general/crds-apis.md --template docs/template/reference-cr.tmpl
# Helm
SRC_ROOT = $(shell git rev-parse --show-toplevel)
helm-docs: HELMDOCS_VERSION := v1.11.0
helm-docs: docker
@docker run -v "$(SRC_ROOT):/helm-docs" jnorwood/helm-docs:$(HELMDOCS_VERSION) --chart-search-root /helm-docs
helm-lint: CT_VERSION := v3.3.1
helm-lint: docker
@docker run -v "$(SRC_ROOT):/workdir" --entrypoint /bin/sh quay.io/helmpack/chart-testing:$(CT_VERSION) -c "cd /workdir; ct lint --config .github/configs/ct.yaml --lint-conf .github/configs/lintconf.yaml --all --debug"
helm-test: kind ct ko-build-all
@kind create cluster --wait=60s --name capsule-charts
@kind load docker-image --name capsule-charts $(LOCAL_CAPSULE_IMG)
@kubectl create ns capsule-system
@ct install --config $(SRC_ROOT)/.github/configs/ct.yaml --namespace=capsule-system --all --debug
@kind delete cluster --name capsule-charts
docker:
@hash docker 2>/dev/null || {\
echo "You need docker" &&\
exit 1;\
}
# Setup development env
# Usage:
# LAPTOP_HOST_IP=<YOUR_LAPTOP_IP> make dev-setup
@@ -148,10 +115,7 @@ dev-setup:
export CA_BUNDLE=`openssl base64 -in /tmp/k8s-webhook-server/serving-certs/tls.crt | tr -d '\n'`; \
kubectl patch MutatingWebhookConfiguration capsule-mutating-webhook-configuration \
--type='json' -p="[\
{'op': 'replace', 'path': '/webhooks/0/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/defaults\",'caBundle':\"$${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/1/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/defaults\",'caBundle':\"$${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/2/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/defaults\",'caBundle':\"$${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/3/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/namespace-owner-reference\",'caBundle':\"$${CA_BUNDLE}\"}}\
{'op': 'replace', 'path': '/webhooks/0/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/mutate-v1-namespace-owner-reference\",'caBundle':\"$${CA_BUNDLE}\"}}\
]" && \
kubectl patch ValidatingWebhookConfiguration capsule-validating-webhook-configuration \
--type='json' -p="[\
@@ -159,117 +123,37 @@ dev-setup:
{'op': 'replace', 'path': '/webhooks/1/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/ingresses\",'caBundle':\"$${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/2/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/namespaces\",'caBundle':\"$${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/3/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/networkpolicies\",'caBundle':\"$${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/4/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/nodes\",'caBundle':\"$${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/5/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/pods\",'caBundle':\"$${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/6/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/persistentvolumeclaims\",'caBundle':\"$${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/7/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/services\",'caBundle':\"$${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/8/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/tenants\",'caBundle':\"$${CA_BUNDLE}\"}}\
]" && \
kubectl patch crd tenants.capsule.clastix.io \
--type='json' -p="[\
{'op': 'replace', 'path': '/spec/conversion/webhook/clientConfig', 'value':{'url': \"$${WEBHOOK_URL}\", 'caBundle': \"$${CA_BUNDLE}\"}}\
]" && \
kubectl patch crd capsuleconfigurations.capsule.clastix.io \
--type='json' -p="[\
{'op': 'replace', 'path': '/spec/conversion/webhook/clientConfig', 'value':{'url': \"$${WEBHOOK_URL}\", 'caBundle': \"$${CA_BUNDLE}\"}}\
{'op': 'replace', 'path': '/webhooks/4/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/pods\",'caBundle':\"$${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/5/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/persistentvolumeclaims\",'caBundle':\"$${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/6/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/services\",'caBundle':\"$${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/7/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/tenants\",'caBundle':\"$${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/8/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/nodes\",'caBundle':\"$${CA_BUNDLE}\"}}\
]";
# Build the docker image
docker-build: test
docker build . -t ${IMG} --build-arg GIT_HEAD_COMMIT=$(GIT_HEAD_COMMIT) \
--build-arg GIT_TAG_COMMIT=$(GIT_TAG_COMMIT) \
--build-arg GIT_MODIFIED=$(GIT_MODIFIED) \
--build-arg GIT_REPO=$(GIT_REPO) \
--build-arg GIT_LAST_TAG=$(VERSION) \
--build-arg BUILD_DATE=$(BUILD_DATE)
####################
# -- Docker
####################
# Push the docker image
docker-push:
docker push ${IMG}
KOCACHE ?= /tmp/ko-cache
KO_REGISTRY := ko.local
KO_TAGS ?= "latest"
ifdef VERSION
KO_TAGS := $(KO_TAGS),$(VERSION)
endif
LD_FLAGS := "-X main.Version=$(VERSION) \
-X main.GitCommit=$(GIT_HEAD_COMMIT) \
-X main.GitTag=$(VERSION) \
-X main.GitTreeState=$(GIT_MODIFIED) \
-X main.BuildDate=$(BUILD_DATE) \
-X main.GitRepo=$(GIT_REPO)"
# Docker Image Build
# ------------------
.PHONY: ko-build-capsule
LOCAL_CAPSULE_IMG_BASE := github.com/$(REPOSITORY)
LOCAL_CAPSULE_IMG := $(KO_REGISTRY)/$(LOCAL_CAPSULE_IMG_BASE)
ko-build-capsule: ko
@echo Building Capsule $(KO_TAGS) >&2
@LD_FLAGS=$(LD_FLAGS) KOCACHE=$(KOCACHE) KO_DOCKER_REPO=$(KO_REGISTRY) \
$(KO) build ./ --preserve-import-paths --tags=$(KO_TAGS) --push=false
.PHONY: ko-build-all
ko-build-all: ko-build-capsule
# Docker Image Publish
# ------------------
REGISTRY_PASSWORD ?= dummy
REGISTRY_USERNAME ?= dummy
.PHONY: ko-login
ko-login: ko
@$(KO) login $(REGISTRY) --username $(REGISTRY_USERNAME) --password $(REGISTRY_PASSWORD)
.PHONY: ko-publish-capsule
ko-publish-capsule: ko-login ## Build and publish kyvernopre image (with ko)
@LD_FLAGS=$(LD_FLAGS) KOCACHE=$(KOCACHE) KO_DOCKER_REPO=$(CAPSULE_IMG) \
$(KO) build ./ --bare --tags=$(KO_TAGS)
.PHONY: ko-publish-all
ko-publish-all: ko-publish-capsule
####################
# -- Binaries
####################
CONTROLLER_GEN := $(shell pwd)/bin/controller-gen
CONTROLLER_GEN_VERSION := v0.10.0
CONTROLLER_GEN = $(shell pwd)/bin/controller-gen
controller-gen: ## Download controller-gen locally if necessary.
$(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_GEN_VERSION))
$(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.5.0)
APIDOCS_GEN := $(shell pwd)/bin/crdoc
APIDOCS_GEN_VERSION := latest
apidocs-gen: ## Download crdoc locally if necessary.
$(call go-install-tool,$(APIDOCS_GEN),fybrik.io/crdoc@$(APIDOCS_GEN_VERSION))
GINKGO := $(shell pwd)/bin/ginkgo
GINGKO_VERSION := v2.9.5
GINKGO = $(shell pwd)/bin/ginkgo
ginkgo: ## Download ginkgo locally if necessary.
$(call go-install-tool,$(GINKGO),github.com/onsi/ginkgo/v2/ginkgo@$(GINGKO_VERSION))
$(call go-install-tool,$(KUSTOMIZE),github.com/onsi/ginkgo/ginkgo@v1.16.5)
CT := $(shell pwd)/bin/ct
CT_VERSION := v3.7.1
ct: ## Download ct locally if necessary.
$(call go-install-tool,$(CT),github.com/helm/chart-testing/v3/ct@$(CT_VERSION))
KIND := $(shell pwd)/bin/kind
KIND_VERSION := v0.17.0
kind: ## Download kind locally if necessary.
$(call go-install-tool,$(KIND),sigs.k8s.io/kind/cmd/kind@$(KIND_VERSION))
KUSTOMIZE := $(shell pwd)/bin/kustomize
KUSTOMIZE_VERSION := 3.8.7
KUSTOMIZE = $(shell pwd)/bin/kustomize
kustomize: ## Download kustomize locally if necessary.
$(call install-kustomize,$(KUSTOMIZE),$(KUSTOMIZE_VERSION))
KO = $(shell pwd)/bin/ko
KO_VERSION = v0.14.1
ko:
$(call go-install-tool,$(KO),github.com/google/ko@v0.14.1)
####################
# -- Helpers
####################
pull-upstream:
git remote add upstream https://github.com/capsuleproject/capsule.git
git fetch --all && git pull upstream
$(call install-kustomize,$(KUSTOMIZE),3.8.7)
define install-kustomize
@[ -f $(1) ] || { \
@@ -286,6 +170,7 @@ PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST))))
define go-install-tool
@[ -f $(1) ] || { \
set -e ;\
echo "Installing $(2)" ;\
GOBIN=$(PROJECT_DIR)/bin go install $(2) ;\
}
endef
@@ -296,32 +181,26 @@ bundle: manifests
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) .
# Sorting imports
.PHONY: goimports
goimports:
goimports -w -l -local "github.com/projectcapsule/capsule" .
GOLANGCI_LINT = $(shell pwd)/bin/golangci-lint
golangci-lint: ## Download golangci-lint locally if necessary.
$(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint@v1.51.2)
goimports -w -l -local "github.com/clastix/capsule" .
# Linting code as PR is expecting
.PHONY: golint
golint: golangci-lint
$(GOLANGCI_LINT) run -c .golangci.yml
golint:
golangci-lint run -c .golangci.yml
# Running e2e tests in a KinD instance
.PHONY: e2e
e2e/%: ginkgo
$(MAKE) e2e-build/$* && $(MAKE) e2e-exec && $(MAKE) e2e-destroy
e2e-build/%:
kind create cluster --wait=60s --name capsule --image=kindest/node:$*
make e2e-load-image
make e2e-install
.PHONY: e2e-install
e2e-install:
kind create cluster --name capsule --image=kindest/node:$*
make docker-build
kind load docker-image --nodes capsule-control-plane --name capsule $(IMG)
helm upgrade \
--debug \
--install \
@@ -330,27 +209,9 @@ e2e-install:
--set 'manager.image.pullPolicy=Never' \
--set 'manager.resources=null'\
--set "manager.image.tag=$(VERSION)" \
--set 'manager.image.registry=$(KO_REGISTRY)' \
--set 'manager.image.repository=$(LOCAL_CAPSULE_IMG_BASE)' \
--set 'manager.livenessProbe.failureThreshold=10' \
--set 'manager.readinessProbe.failureThreshold=10' \
--set 'podSecurityContext.seccompProfile=null' \
capsule \
./charts/capsule
.PHONY: e2e-load-image
e2e-load-image: ko-build-all
kind load docker-image --nodes capsule-control-plane --name capsule $(LOCAL_CAPSULE_IMG):$(VERSION)
.PHONY: e2e-exec
e2e-exec: ginkgo
$(GINKGO) -v -tags e2e ./e2e
.PHONY: e2e-destroy
e2e-destroy:
kind delete cluster --name capsule
SPELL_CHECKER = npx spellchecker-cli
docs-lint:
cd docs/content && $(SPELL_CHECKER) -f "*.md" "*/*.md" -d dictionary.txt

41
PROJECT
View File

@@ -5,62 +5,35 @@ plugins:
manifests.sdk.operatorframework.io/v2: {}
scorecard.sdk.operatorframework.io/v2: {}
projectName: capsule
repo: github.com/projectcapsule/capsule
repo: github.com/clastix/capsule
resources:
- api:
crdVersion: v1
namespaced: false
controller: true
domain: clastix.io
group: capsule
kind: Tenant
path: github.com/projectcapsule/capsule/api/v1alpha1
path: github.com/clastix/capsule/api/v1alpha1
version: v1alpha1
webhooks:
conversion: true
webhookVersion: v1
- api:
crdVersion: v1
namespaced: false
controller: true
domain: clastix.io
group: capsule
kind: CapsuleConfiguration
path: github.com/projectcapsule/capsule/api/v1alpha1
path: github.com/clastix/capsule/api/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
namespaced: false
domain: clastix.io
group: capsule
kind: Tenant
path: github.com/projectcapsule/capsule/api/v1beta1
path: github.com/clastix/capsule/api/v1beta1
version: v1beta1
- api:
crdVersion: v1
domain: clastix.io
group: capsule
kind: Tenant
path: github.com/projectcapsule/capsule/api/v1beta2
version: v1beta2
- api:
crdVersion: v1
controller: true
domain: clastix.io
group: capsule
kind: CapsuleConfiguration
path: github.com/projectcapsule/capsule/api/v1beta2
version: v1beta2
- api:
crdVersion: v1
namespaced: true
domain: clastix.io
group: capsule
kind: TenantResource
path: github.com/projectcapsule/capsule/api/v1beta2
version: v1beta2
- api:
crdVersion: v1
domain: clastix.io
group: capsule
kind: GlobalTenantResource
path: github.com/projectcapsule/capsule/api/v1beta2
version: v1beta2
version: "3"

View File

@@ -1,20 +1,13 @@
<p align="left">
<img src="https://github.com/projectcapsule/capsule/actions/workflows/ci.yml/badge.svg"/>
<img src="https://img.shields.io/github/license/clastix/capsule"/>
<img src="https://img.shields.io/github/go-mod/go-version/clastix/capsule"/>
<a href="https://github.com/projectcapsule/capsule/releases">
<a href="https://github.com/clastix/capsule/releases">
<img src="https://img.shields.io/github/v/release/clastix/capsule"/>
</a>
<a href="https://charmhub.io/capsule-k8s">
<img src="https://charmhub.io/capsule-k8s/badge.svg"/>
</a>
<a href="https://www.bestpractices.dev/projects/5601">
<img src="https://www.bestpractices.dev/projects/5601/badge"/>
</a>
<a href="https://api.securityscorecards.dev/projects/github.com/projectcapsule/capsule/badge">
<img src="https://api.securityscorecards.dev/projects/github.com/projectcapsule/capsule/badge"/>
</a>
</p>
<p align="center">
@@ -79,27 +72,17 @@ Capsule is Open Source with Apache 2 license and any contribution is welcome.
## Chart Development
### Chart Linting
The documentation for each chart is done with [helm-docs](https://github.com/norwoodj/helm-docs). This way we can ensure that values are consistent with the chart documentation.
The chart is linted with [ct](https://github.com/helm/chart-testing). You can run the linter locally with this command:
We have a script on the repository which will execute the helm-docs docker container, so that you don't have to worry about downloading the binary etc. Simply execute the script (Bash compatible):
```
make helm-lint
```
### Chart Documentation
The documentation for each chart is done with [helm-docs](https://github.com/norwoodj/helm-docs). This way we can ensure that values are consistent with the chart documentation. Run this anytime you make changes to a `values.yaml` file:
```
make helm-docs
bash scripts/helm-docs.sh
```
## Community
Join the community, share and learn from it. You can find all the resources to how to contribute code and docs, connect with people in the [community repository](https://github.com/projectcapsule/capsule-community).
Please read the [code of conduct](CODE_OF_CONDUCT.md).
Join the community, share and learn from it. You can find all the resources to how to contribute code and docs, connect with people in the [community repository](https://github.com/clastix/capsule-community).
## Adopters
@@ -113,10 +96,6 @@ You can find how the Capsule project is governed [here](https://capsule.clastix.
Please, refer to the maintainers file available [here](.github/maintainers.yaml).
## Release process
Please, refer to the [documentation page](https://capsule.clastix.io/docs/contributing/release).
# FAQ
- Q. How to pronounce Capsule?
@@ -125,7 +104,7 @@ Please, refer to the [documentation page](https://capsule.clastix.io/docs/contri
- Q. Is it production grade?
A. Although under frequent development and improvements, Capsule is ready to be used in production environments as currently, people are using it in public and private deployments. Check out the [release](https://github.com/projectcapsule/capsule/releases) page for a detailed list of available versions.
A. Although under frequent development and improvements, Capsule is ready to be used in production environments as currently, people are using it in public and private deployments. Check out the [release](https://github.com/clastix/capsule/releases) page for a detailed list of available versions.
- Q. Does it work with my Kubernetes XYZ distribution?

View File

@@ -1,9 +1,9 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
type AdditionalMetadata struct {
Labels map[string]string `json:"additionalLabels,omitempty"`
Annotations map[string]string `json:"additionalAnnotations,omitempty"`
type AdditionalMetadataSpec struct {
AdditionalLabels map[string]string `json:"additionalLabels,omitempty"`
AdditionalAnnotations map[string]string `json:"additionalAnnotations,omitempty"`
}

View File

@@ -1,7 +1,7 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta2
package v1alpha1
import rbacv1 "k8s.io/api/rbac/v1"

View File

@@ -0,0 +1,37 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import (
"regexp"
"sort"
"strings"
)
type AllowedListSpec struct {
Exact []string `json:"allowed,omitempty"`
Regex string `json:"allowedRegex,omitempty"`
}
func (in *AllowedListSpec) ExactMatch(value string) (ok bool) {
if len(in.Exact) > 0 {
sort.SliceStable(in.Exact, func(i, j int) bool {
return strings.ToLower(in.Exact[i]) < strings.ToLower(in.Exact[j])
})
i := sort.SearchStrings(in.Exact, value)
ok = i < len(in.Exact) && in.Exact[i] == value
}
return
}
func (in AllowedListSpec) RegexMatch(value string) (ok bool) {
if len(in.Regex) > 0 {
ok = regexp.MustCompile(in.Regex).MatchString(value)
}
return
}

View File

@@ -0,0 +1,73 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAllowedListSpec_ExactMatch(t *testing.T) {
type tc struct {
In []string
True []string
False []string
}
for _, tc := range []tc{
{
[]string{"foo", "bar", "bizz", "buzz"},
[]string{"foo", "bar", "bizz", "buzz"},
[]string{"bing", "bong"},
},
{
[]string{"one", "two", "three"},
[]string{"one", "two", "three"},
[]string{"a", "b", "c"},
},
{
nil,
nil,
[]string{"any", "value"},
},
} {
a := AllowedListSpec{
Exact: tc.In,
}
for _, ok := range tc.True {
assert.True(t, a.ExactMatch(ok))
}
for _, ko := range tc.False {
assert.False(t, a.ExactMatch(ko))
}
}
}
func TestAllowedListSpec_RegexMatch(t *testing.T) {
type tc struct {
Regex string
True []string
False []string
}
for _, tc := range []tc{
{`first-\w+-pattern`, []string{"first-date-pattern", "first-year-pattern"}, []string{"broken", "first-year", "second-date-pattern"}},
{``, nil, []string{"any", "value"}},
} {
a := AllowedListSpec{
Regex: tc.Regex,
}
for _, ok := range tc.True {
assert.True(t, a.RegexMatch(ok))
}
for _, ko := range tc.False {
assert.False(t, a.RegexMatch(ko))
}
}
}

View File

@@ -1,6 +1,3 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
const (

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
@@ -31,8 +31,6 @@ type CapsuleConfiguration struct {
Spec CapsuleConfigurationSpec `json:"spec,omitempty"`
}
func (in *CapsuleConfiguration) Hub() {}
// +kubebuilder:object:root=true
// CapsuleConfigurationList contains a list of CapsuleConfiguration.

View File

@@ -1,21 +0,0 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import (
"os"
ctrl "sigs.k8s.io/controller-runtime"
)
func (in *CapsuleConfiguration) SetupWebhookWithManager(mgr ctrl.Manager) error {
certData, _ := os.ReadFile("/tmp/k8s-webhook-server/serving-certs/tls.crt")
if len(certData) == 0 {
return nil
}
return ctrl.NewWebhookManagedBy(mgr).
For(in).
Complete()
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
@@ -13,8 +13,7 @@ import (
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/conversion"
capsulev1beta1 "github.com/projectcapsule/capsule/api/v1beta1"
"github.com/projectcapsule/capsule/pkg/api"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
const (
@@ -49,7 +48,7 @@ const (
ingressHostnameCollisionScope = "ingress.capsule.clastix.io/hostname-collision-scope"
)
func (in *Tenant) convertV1Alpha1OwnerToV1Beta1() capsulev1beta1.OwnerListSpec {
func (t *Tenant) convertV1Alpha1OwnerToV1Beta1() capsulev1beta1.OwnerListSpec {
serviceKindToAnnotationMap := map[capsulev1beta1.ProxyServiceKind][]string{
capsulev1beta1.NodesProxy: {enableNodeListingAnnotation, enableNodeUpdateAnnotation, enableNodeDeletionAnnotation},
capsulev1beta1.StorageClassesProxy: {enableStorageClassListingAnnotation, enableStorageClassUpdateAnnotation, enableStorageClassDeletionAnnotation},
@@ -76,7 +75,7 @@ func (in *Tenant) convertV1Alpha1OwnerToV1Beta1() capsulev1beta1.OwnerListSpec {
ownerServiceAccountAnnotation: capsulev1beta1.ServiceAccountOwner,
}
annotations := in.GetAnnotations()
annotations := t.GetAnnotations()
operations := make(map[string]map[capsulev1beta1.ProxyServiceKind][]capsulev1beta1.ProxyOperation)
@@ -112,9 +111,9 @@ func (in *Tenant) convertV1Alpha1OwnerToV1Beta1() capsulev1beta1.OwnerListSpec {
}
owners = append(owners, capsulev1beta1.OwnerSpec{
Kind: capsulev1beta1.OwnerKind(in.Spec.Owner.Kind),
Name: in.Spec.Owner.Name,
ProxyOperations: getProxySettingsForOwner(in.Spec.Owner.Name),
Kind: capsulev1beta1.OwnerKind(t.Spec.Owner.Kind),
Name: t.Spec.Owner.Name,
ProxyOperations: getProxySettingsForOwner(t.Spec.Owner.Name),
})
for ownerAnnotation, ownerKind := range annotationToOwnerKindMap {
@@ -133,135 +132,151 @@ func (in *Tenant) convertV1Alpha1OwnerToV1Beta1() capsulev1beta1.OwnerListSpec {
return owners
}
//nolint:gocognit,gocyclo,cyclop,maintidx
func (in *Tenant) ConvertTo(dstRaw conversion.Hub) error {
// nolint:gocognit,gocyclo,cyclop,maintidx
func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
dst, ok := dstRaw.(*capsulev1beta1.Tenant)
if !ok {
return fmt.Errorf("expected type *capsulev1beta1.Tenant, got %T", dst)
}
annotations := in.GetAnnotations()
annotations := t.GetAnnotations()
// ObjectMeta
dst.ObjectMeta = in.ObjectMeta
dst.ObjectMeta = t.ObjectMeta
// Spec
if in.Spec.NamespaceQuota != nil {
if t.Spec.NamespaceQuota != nil {
if dst.Spec.NamespaceOptions == nil {
dst.Spec.NamespaceOptions = &capsulev1beta1.NamespaceOptions{}
}
dst.Spec.NamespaceOptions.Quota = in.Spec.NamespaceQuota
dst.Spec.NamespaceOptions.Quota = t.Spec.NamespaceQuota
}
dst.Spec.NodeSelector = in.Spec.NodeSelector
dst.Spec.NodeSelector = t.Spec.NodeSelector
dst.Spec.Owners = in.convertV1Alpha1OwnerToV1Beta1()
dst.Spec.Owners = t.convertV1Alpha1OwnerToV1Beta1()
if in.Spec.NamespacesMetadata != nil {
if t.Spec.NamespacesMetadata != nil {
if dst.Spec.NamespaceOptions == nil {
dst.Spec.NamespaceOptions = &capsulev1beta1.NamespaceOptions{}
}
dst.Spec.NamespaceOptions.AdditionalMetadata = &api.AdditionalMetadataSpec{
Labels: in.Spec.NamespacesMetadata.Labels,
Annotations: in.Spec.NamespacesMetadata.Annotations,
dst.Spec.NamespaceOptions.AdditionalMetadata = &capsulev1beta1.AdditionalMetadataSpec{
Labels: t.Spec.NamespacesMetadata.AdditionalLabels,
Annotations: t.Spec.NamespacesMetadata.AdditionalAnnotations,
}
}
if in.Spec.ServicesMetadata != nil {
if t.Spec.ServicesMetadata != nil {
if dst.Spec.ServiceOptions == nil {
dst.Spec.ServiceOptions = &api.ServiceOptions{}
}
dst.Spec.ServiceOptions.AdditionalMetadata = &api.AdditionalMetadataSpec{
Labels: in.Spec.ServicesMetadata.Labels,
Annotations: in.Spec.ServicesMetadata.Annotations,
dst.Spec.ServiceOptions = &capsulev1beta1.ServiceOptions{
AdditionalMetadata: &capsulev1beta1.AdditionalMetadataSpec{
Labels: t.Spec.ServicesMetadata.AdditionalLabels,
Annotations: t.Spec.ServicesMetadata.AdditionalAnnotations,
},
}
}
}
if in.Spec.StorageClasses != nil {
dst.Spec.StorageClasses = in.Spec.StorageClasses
if t.Spec.StorageClasses != nil {
dst.Spec.StorageClasses = &capsulev1beta1.AllowedListSpec{
Exact: t.Spec.StorageClasses.Exact,
Regex: t.Spec.StorageClasses.Regex,
}
}
if v, annotationOk := in.Annotations[ingressHostnameCollisionScope]; annotationOk {
if v, annotationOk := t.Annotations[ingressHostnameCollisionScope]; annotationOk {
switch v {
case string(api.HostnameCollisionScopeCluster), string(api.HostnameCollisionScopeTenant), string(api.HostnameCollisionScopeNamespace):
dst.Spec.IngressOptions.HostnameCollisionScope = api.HostnameCollisionScope(v)
case string(capsulev1beta1.HostnameCollisionScopeCluster), string(capsulev1beta1.HostnameCollisionScopeTenant), string(capsulev1beta1.HostnameCollisionScopeNamespace):
dst.Spec.IngressOptions.HostnameCollisionScope = capsulev1beta1.HostnameCollisionScope(v)
default:
dst.Spec.IngressOptions.HostnameCollisionScope = api.HostnameCollisionScopeDisabled
dst.Spec.IngressOptions.HostnameCollisionScope = capsulev1beta1.HostnameCollisionScopeDisabled
}
}
if in.Spec.IngressClasses != nil {
dst.Spec.IngressOptions.AllowedClasses = &api.AllowedListSpec{
Exact: in.Spec.IngressClasses.Exact,
Regex: in.Spec.IngressClasses.Regex,
if t.Spec.IngressClasses != nil {
dst.Spec.IngressOptions.AllowedClasses = &capsulev1beta1.AllowedListSpec{
Exact: t.Spec.IngressClasses.Exact,
Regex: t.Spec.IngressClasses.Regex,
}
}
if in.Spec.IngressHostnames != nil {
dst.Spec.IngressOptions.AllowedHostnames = &api.AllowedListSpec{
Exact: in.Spec.IngressHostnames.Exact,
Regex: in.Spec.IngressHostnames.Regex,
if t.Spec.IngressHostnames != nil {
dst.Spec.IngressOptions.AllowedHostnames = &capsulev1beta1.AllowedListSpec{
Exact: t.Spec.IngressHostnames.Exact,
Regex: t.Spec.IngressHostnames.Regex,
}
}
if in.Spec.ContainerRegistries != nil {
dst.Spec.ContainerRegistries = &api.AllowedListSpec{
Exact: in.Spec.ContainerRegistries.Exact,
Regex: in.Spec.ContainerRegistries.Regex,
if t.Spec.ContainerRegistries != nil {
dst.Spec.ContainerRegistries = &capsulev1beta1.AllowedListSpec{
Exact: t.Spec.ContainerRegistries.Exact,
Regex: t.Spec.ContainerRegistries.Regex,
}
}
if len(in.Spec.NetworkPolicies) > 0 {
dst.Spec.NetworkPolicies = api.NetworkPolicySpec{
Items: in.Spec.NetworkPolicies,
if len(t.Spec.NetworkPolicies) > 0 {
dst.Spec.NetworkPolicies = capsulev1beta1.NetworkPolicySpec{
Items: t.Spec.NetworkPolicies,
}
}
if len(in.Spec.LimitRanges) > 0 {
dst.Spec.LimitRanges = api.LimitRangesSpec{
Items: in.Spec.LimitRanges,
if len(t.Spec.LimitRanges) > 0 {
dst.Spec.LimitRanges = capsulev1beta1.LimitRangesSpec{
Items: t.Spec.LimitRanges,
}
}
if len(in.Spec.ResourceQuota) > 0 {
dst.Spec.ResourceQuota = api.ResourceQuotaSpec{
Scope: func() api.ResourceQuotaScope {
if v, annotationOk := in.GetAnnotations()[resourceQuotaScopeAnnotation]; annotationOk {
if len(t.Spec.ResourceQuota) > 0 {
dst.Spec.ResourceQuota = capsulev1beta1.ResourceQuotaSpec{
Scope: func() capsulev1beta1.ResourceQuotaScope {
if v, annotationOk := t.GetAnnotations()[resourceQuotaScopeAnnotation]; annotationOk {
switch v {
case string(api.ResourceQuotaScopeNamespace):
return api.ResourceQuotaScopeNamespace
case string(api.ResourceQuotaScopeTenant):
return api.ResourceQuotaScopeTenant
case string(capsulev1beta1.ResourceQuotaScopeNamespace):
return capsulev1beta1.ResourceQuotaScopeNamespace
case string(capsulev1beta1.ResourceQuotaScopeTenant):
return capsulev1beta1.ResourceQuotaScopeTenant
}
}
return api.ResourceQuotaScopeTenant
return capsulev1beta1.ResourceQuotaScopeTenant
}(),
Items: in.Spec.ResourceQuota,
Items: t.Spec.ResourceQuota,
}
}
dst.Spec.AdditionalRoleBindings = in.Spec.AdditionalRoleBindings
if len(t.Spec.AdditionalRoleBindings) > 0 {
for _, rb := range t.Spec.AdditionalRoleBindings {
dst.Spec.AdditionalRoleBindings = append(dst.Spec.AdditionalRoleBindings, capsulev1beta1.AdditionalRoleBindingsSpec{
ClusterRoleName: rb.ClusterRoleName,
Subjects: rb.Subjects,
})
}
}
if in.Spec.ExternalServiceIPs != nil {
if t.Spec.ExternalServiceIPs != nil {
if dst.Spec.ServiceOptions == nil {
dst.Spec.ServiceOptions = &api.ServiceOptions{}
dst.Spec.ServiceOptions = &capsulev1beta1.ServiceOptions{}
}
dst.Spec.ServiceOptions.ExternalServiceIPs = in.Spec.ExternalServiceIPs
dst.Spec.ServiceOptions.ExternalServiceIPs = &capsulev1beta1.ExternalServiceIPsSpec{
Allowed: make([]capsulev1beta1.AllowedIP, len(t.Spec.ExternalServiceIPs.Allowed)),
}
for i, IP := range t.Spec.ExternalServiceIPs.Allowed {
dst.Spec.ServiceOptions.ExternalServiceIPs.Allowed[i] = capsulev1beta1.AllowedIP(IP)
}
}
pullPolicies, ok := annotations[podAllowedImagePullPolicyAnnotation]
if ok {
for _, policy := range strings.Split(pullPolicies, ",") {
dst.Spec.ImagePullPolicies = append(dst.Spec.ImagePullPolicies, api.ImagePullPolicySpec(policy))
dst.Spec.ImagePullPolicies = append(dst.Spec.ImagePullPolicies, capsulev1beta1.ImagePullPolicySpec(policy))
}
}
priorityClasses := api.AllowedListSpec{}
priorityClasses := capsulev1beta1.AllowedListSpec{}
priorityClassAllowed, ok := annotations[podPriorityAllowedAnnotation]
@@ -283,59 +298,59 @@ func (in *Tenant) ConvertTo(dstRaw conversion.Hub) error {
if ok {
val, err := strconv.ParseBool(enableNodePorts)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("unable to parse %s annotation on tenant %s", enableNodePortsAnnotation, in.GetName()))
return errors.Wrap(err, fmt.Sprintf("unable to parse %s annotation on tenant %s", enableNodePortsAnnotation, t.GetName()))
}
if dst.Spec.ServiceOptions == nil {
dst.Spec.ServiceOptions = &api.ServiceOptions{}
dst.Spec.ServiceOptions = &capsulev1beta1.ServiceOptions{}
}
if dst.Spec.ServiceOptions.AllowedServices == nil {
dst.Spec.ServiceOptions.AllowedServices = &api.AllowedServices{}
dst.Spec.ServiceOptions.AllowedServices = &capsulev1beta1.AllowedServices{}
}
dst.Spec.ServiceOptions.AllowedServices.NodePort = pointer.Bool(val)
dst.Spec.ServiceOptions.AllowedServices.NodePort = pointer.BoolPtr(val)
}
enableExternalName, ok := annotations[enableExternalNameAnnotation]
if ok {
val, err := strconv.ParseBool(enableExternalName)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("unable to parse %s annotation on tenant %s", enableExternalNameAnnotation, in.GetName()))
return errors.Wrap(err, fmt.Sprintf("unable to parse %s annotation on tenant %s", enableExternalNameAnnotation, t.GetName()))
}
if dst.Spec.ServiceOptions == nil {
dst.Spec.ServiceOptions = &api.ServiceOptions{}
dst.Spec.ServiceOptions = &capsulev1beta1.ServiceOptions{}
}
if dst.Spec.ServiceOptions.AllowedServices == nil {
dst.Spec.ServiceOptions.AllowedServices = &api.AllowedServices{}
dst.Spec.ServiceOptions.AllowedServices = &capsulev1beta1.AllowedServices{}
}
dst.Spec.ServiceOptions.AllowedServices.ExternalName = pointer.Bool(val)
dst.Spec.ServiceOptions.AllowedServices.ExternalName = pointer.BoolPtr(val)
}
loadBalancerService, ok := annotations[enableLoadBalancerAnnotation]
if ok {
val, err := strconv.ParseBool(loadBalancerService)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("unable to parse %s annotation on tenant %s", enableLoadBalancerAnnotation, in.GetName()))
return errors.Wrap(err, fmt.Sprintf("unable to parse %s annotation on tenant %s", enableLoadBalancerAnnotation, t.GetName()))
}
if dst.Spec.ServiceOptions == nil {
dst.Spec.ServiceOptions = &api.ServiceOptions{}
dst.Spec.ServiceOptions = &capsulev1beta1.ServiceOptions{}
}
if dst.Spec.ServiceOptions.AllowedServices == nil {
dst.Spec.ServiceOptions.AllowedServices = &api.AllowedServices{}
dst.Spec.ServiceOptions.AllowedServices = &capsulev1beta1.AllowedServices{}
}
dst.Spec.ServiceOptions.AllowedServices.LoadBalancer = pointer.Bool(val)
dst.Spec.ServiceOptions.AllowedServices.LoadBalancer = pointer.BoolPtr(val)
}
// Status
dst.Status = capsulev1beta1.TenantStatus{
Size: in.Status.Size,
Namespaces: in.Status.Namespaces,
Size: t.Status.Size,
Namespaces: t.Status.Namespaces,
}
// Remove unneeded annotations
delete(dst.ObjectMeta.Annotations, podAllowedImagePullPolicyAnnotation)
@@ -365,8 +380,8 @@ func (in *Tenant) ConvertTo(dstRaw conversion.Hub) error {
return nil
}
//nolint:gocognit,gocyclo,cyclop
func (in *Tenant) convertV1Beta1OwnerToV1Alpha1(src *capsulev1beta1.Tenant) {
// nolint:gocognit,gocyclo,cyclop
func (t *Tenant) convertV1Beta1OwnerToV1Alpha1(src *capsulev1beta1.Tenant) {
ownersAnnotations := map[string][]string{
ownerGroupsAnnotation: nil,
ownerUsersAnnotation: nil,
@@ -387,7 +402,7 @@ func (in *Tenant) convertV1Beta1OwnerToV1Alpha1(src *capsulev1beta1.Tenant) {
for i, owner := range src.Spec.Owners {
if i == 0 {
in.Spec.Owner = OwnerSpec{
t.Spec.Owner = OwnerSpec{
Name: owner.Name,
Kind: Kind(owner.Kind),
}
@@ -454,89 +469,114 @@ func (in *Tenant) convertV1Beta1OwnerToV1Alpha1(src *capsulev1beta1.Tenant) {
for k, v := range ownersAnnotations {
if len(v) > 0 {
in.Annotations[k] = strings.Join(v, ",")
t.Annotations[k] = strings.Join(v, ",")
}
}
for k, v := range proxyAnnotations {
if len(v) > 0 {
in.Annotations[k] = strings.Join(v, ",")
t.Annotations[k] = strings.Join(v, ",")
}
}
}
//nolint:cyclop
func (in *Tenant) ConvertFrom(srcRaw conversion.Hub) error {
// nolint:gocyclo,cyclop
func (t *Tenant) ConvertFrom(srcRaw conversion.Hub) error {
src, ok := srcRaw.(*capsulev1beta1.Tenant)
if !ok {
return fmt.Errorf("expected *capsulev1beta1.Tenant, got %T", srcRaw)
}
// ObjectMeta
in.ObjectMeta = src.ObjectMeta
t.ObjectMeta = src.ObjectMeta
// Spec
if src.Spec.NamespaceOptions != nil && src.Spec.NamespaceOptions.Quota != nil {
in.Spec.NamespaceQuota = src.Spec.NamespaceOptions.Quota
t.Spec.NamespaceQuota = src.Spec.NamespaceOptions.Quota
}
in.Spec.NodeSelector = src.Spec.NodeSelector
t.Spec.NodeSelector = src.Spec.NodeSelector
if in.Annotations == nil {
in.Annotations = make(map[string]string)
if t.Annotations == nil {
t.Annotations = make(map[string]string)
}
in.convertV1Beta1OwnerToV1Alpha1(src)
t.convertV1Beta1OwnerToV1Alpha1(src)
if src.Spec.NamespaceOptions != nil && src.Spec.NamespaceOptions.AdditionalMetadata != nil {
in.Spec.NamespacesMetadata = &AdditionalMetadata{
Labels: src.Spec.NamespaceOptions.AdditionalMetadata.Labels,
Annotations: src.Spec.NamespaceOptions.AdditionalMetadata.Annotations,
t.Spec.NamespacesMetadata = &AdditionalMetadataSpec{
AdditionalLabels: src.Spec.NamespaceOptions.AdditionalMetadata.Labels,
AdditionalAnnotations: src.Spec.NamespaceOptions.AdditionalMetadata.Annotations,
}
}
if src.Spec.ServiceOptions != nil && src.Spec.ServiceOptions.AdditionalMetadata != nil {
in.Spec.ServicesMetadata = &AdditionalMetadata{
Labels: src.Spec.ServiceOptions.AdditionalMetadata.Labels,
Annotations: src.Spec.ServiceOptions.AdditionalMetadata.Annotations,
t.Spec.ServicesMetadata = &AdditionalMetadataSpec{
AdditionalLabels: src.Spec.ServiceOptions.AdditionalMetadata.Labels,
AdditionalAnnotations: src.Spec.ServiceOptions.AdditionalMetadata.Annotations,
}
}
if src.Spec.StorageClasses != nil {
in.Spec.StorageClasses = src.Spec.StorageClasses
t.Spec.StorageClasses = &AllowedListSpec{
Exact: src.Spec.StorageClasses.Exact,
Regex: src.Spec.StorageClasses.Regex,
}
}
in.Annotations[ingressHostnameCollisionScope] = string(src.Spec.IngressOptions.HostnameCollisionScope)
t.Annotations[ingressHostnameCollisionScope] = string(src.Spec.IngressOptions.HostnameCollisionScope)
if src.Spec.IngressOptions.AllowedClasses != nil {
in.Spec.IngressClasses = src.Spec.IngressOptions.AllowedClasses
t.Spec.IngressClasses = &AllowedListSpec{
Exact: src.Spec.IngressOptions.AllowedClasses.Exact,
Regex: src.Spec.IngressOptions.AllowedClasses.Regex,
}
}
if src.Spec.IngressOptions.AllowedHostnames != nil {
in.Spec.IngressHostnames = src.Spec.IngressOptions.AllowedHostnames
t.Spec.IngressHostnames = &AllowedListSpec{
Exact: src.Spec.IngressOptions.AllowedHostnames.Exact,
Regex: src.Spec.IngressOptions.AllowedHostnames.Regex,
}
}
if src.Spec.ContainerRegistries != nil {
in.Spec.ContainerRegistries = src.Spec.ContainerRegistries
t.Spec.ContainerRegistries = &AllowedListSpec{
Exact: src.Spec.ContainerRegistries.Exact,
Regex: src.Spec.ContainerRegistries.Regex,
}
}
if len(src.Spec.NetworkPolicies.Items) > 0 {
in.Spec.NetworkPolicies = src.Spec.NetworkPolicies.Items
t.Spec.NetworkPolicies = src.Spec.NetworkPolicies.Items
}
if len(src.Spec.LimitRanges.Items) > 0 {
in.Spec.LimitRanges = src.Spec.LimitRanges.Items
t.Spec.LimitRanges = src.Spec.LimitRanges.Items
}
if len(src.Spec.ResourceQuota.Items) > 0 {
in.Annotations[resourceQuotaScopeAnnotation] = string(src.Spec.ResourceQuota.Scope)
in.Spec.ResourceQuota = src.Spec.ResourceQuota.Items
t.Annotations[resourceQuotaScopeAnnotation] = string(src.Spec.ResourceQuota.Scope)
t.Spec.ResourceQuota = src.Spec.ResourceQuota.Items
}
in.Spec.AdditionalRoleBindings = src.Spec.AdditionalRoleBindings
if len(src.Spec.AdditionalRoleBindings) > 0 {
for _, rb := range src.Spec.AdditionalRoleBindings {
t.Spec.AdditionalRoleBindings = append(t.Spec.AdditionalRoleBindings, AdditionalRoleBindingsSpec{
ClusterRoleName: rb.ClusterRoleName,
Subjects: rb.Subjects,
})
}
}
if src.Spec.ServiceOptions != nil && src.Spec.ServiceOptions.ExternalServiceIPs != nil {
in.Spec.ExternalServiceIPs = src.Spec.ServiceOptions.ExternalServiceIPs
t.Spec.ExternalServiceIPs = &ExternalServiceIPsSpec{
Allowed: make([]AllowedIP, len(src.Spec.ServiceOptions.ExternalServiceIPs.Allowed)),
}
for i, IP := range src.Spec.ServiceOptions.ExternalServiceIPs.Allowed {
t.Spec.ExternalServiceIPs.Allowed[i] = AllowedIP(IP)
}
}
if len(src.Spec.ImagePullPolicies) != 0 {
@@ -546,35 +586,35 @@ func (in *Tenant) ConvertFrom(srcRaw conversion.Hub) error {
pullPolicies = append(pullPolicies, string(policy))
}
in.Annotations[podAllowedImagePullPolicyAnnotation] = strings.Join(pullPolicies, ",")
t.Annotations[podAllowedImagePullPolicyAnnotation] = strings.Join(pullPolicies, ",")
}
if src.Spec.PriorityClasses != nil {
if len(src.Spec.PriorityClasses.Exact) != 0 {
in.Annotations[podPriorityAllowedAnnotation] = strings.Join(src.Spec.PriorityClasses.Exact, ",")
t.Annotations[podPriorityAllowedAnnotation] = strings.Join(src.Spec.PriorityClasses.Exact, ",")
}
if src.Spec.PriorityClasses.Regex != "" {
in.Annotations[podPriorityAllowedRegexAnnotation] = src.Spec.PriorityClasses.Regex
t.Annotations[podPriorityAllowedRegexAnnotation] = src.Spec.PriorityClasses.Regex
}
}
if src.Spec.ServiceOptions != nil && src.Spec.ServiceOptions.AllowedServices != nil {
if src.Spec.ServiceOptions.AllowedServices.NodePort != nil {
in.Annotations[enableNodePortsAnnotation] = strconv.FormatBool(*src.Spec.ServiceOptions.AllowedServices.NodePort)
t.Annotations[enableNodePortsAnnotation] = strconv.FormatBool(*src.Spec.ServiceOptions.AllowedServices.NodePort)
}
if src.Spec.ServiceOptions.AllowedServices.ExternalName != nil {
in.Annotations[enableExternalNameAnnotation] = strconv.FormatBool(*src.Spec.ServiceOptions.AllowedServices.ExternalName)
t.Annotations[enableExternalNameAnnotation] = strconv.FormatBool(*src.Spec.ServiceOptions.AllowedServices.ExternalName)
}
if src.Spec.ServiceOptions.AllowedServices.LoadBalancer != nil {
in.Annotations[enableLoadBalancerAnnotation] = strconv.FormatBool(*src.Spec.ServiceOptions.AllowedServices.LoadBalancer)
t.Annotations[enableLoadBalancerAnnotation] = strconv.FormatBool(*src.Spec.ServiceOptions.AllowedServices.LoadBalancer)
}
}
// Status
in.Status = TenantStatus{
t.Status = TenantStatus{
Size: src.Status.Size,
Namespaces: src.Status.Namespaces,
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
@@ -15,30 +15,29 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/pointer"
capsulev1beta1 "github.com/projectcapsule/capsule/api/v1beta1"
"github.com/projectcapsule/capsule/pkg/api"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
//nolint:maintidx
// nolint:maintidx
func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
var namespaceQuota int32 = 5
nodeSelector := map[string]string{
"foo": "bar",
}
v1alpha1AdditionalMetadataSpec := &AdditionalMetadata{
Labels: map[string]string{
v1alpha1AdditionalMetadataSpec := &AdditionalMetadataSpec{
AdditionalLabels: map[string]string{
"foo": "bar",
},
Annotations: map[string]string{
AdditionalAnnotations: map[string]string{
"foo": "bar",
},
}
v1alpha1AllowedListSpec := &api.AllowedListSpec{
v1alpha1AllowedListSpec := &AllowedListSpec{
Exact: []string{"foo", "bar"},
Regex: "^foo*",
}
v1beta1AdditionalMetadataSpec := &api.AdditionalMetadataSpec{
v1beta1AdditionalMetadataSpec := &capsulev1beta1.AdditionalMetadataSpec{
Labels: map[string]string{
"foo": "bar",
},
@@ -50,22 +49,20 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
Quota: &namespaceQuota,
AdditionalMetadata: v1beta1AdditionalMetadataSpec,
}
v1beta1ServiceOptions := &api.ServiceOptions{
v1beta1ServiceOptions := &capsulev1beta1.ServiceOptions{
AdditionalMetadata: v1beta1AdditionalMetadataSpec,
AllowedServices: &api.AllowedServices{
NodePort: pointer.Bool(false),
ExternalName: pointer.Bool(false),
LoadBalancer: pointer.Bool(false),
AllowedServices: &capsulev1beta1.AllowedServices{
NodePort: pointer.BoolPtr(false),
ExternalName: pointer.BoolPtr(false),
LoadBalancer: pointer.BoolPtr(false),
},
ExternalServiceIPs: &api.ExternalServiceIPsSpec{
Allowed: []api.AllowedIP{"192.168.0.1"},
ExternalServiceIPs: &capsulev1beta1.ExternalServiceIPsSpec{
Allowed: []capsulev1beta1.AllowedIP{"192.168.0.1"},
},
}
v1beta2AllowedListSpec := &api.SelectorAllowedListSpec{
AllowedListSpec: api.AllowedListSpec{
Exact: []string{"foo", "bar"},
Regex: "^foo*",
},
v1beta1AllowedListSpec := &capsulev1beta1.AllowedListSpec{
Exact: []string{"foo", "bar"},
Regex: "^foo*",
}
networkPolicies := []networkingv1.NetworkPolicySpec{
{
@@ -237,25 +234,25 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
},
NamespaceOptions: v1beta1NamespaceOptions,
ServiceOptions: v1beta1ServiceOptions,
StorageClasses: &v1beta2AllowedListSpec.AllowedListSpec,
StorageClasses: v1beta1AllowedListSpec,
IngressOptions: capsulev1beta1.IngressOptions{
HostnameCollisionScope: api.HostnameCollisionScopeDisabled,
AllowedClasses: &v1beta2AllowedListSpec.AllowedListSpec,
AllowedHostnames: &v1beta2AllowedListSpec.AllowedListSpec,
HostnameCollisionScope: capsulev1beta1.HostnameCollisionScopeDisabled,
AllowedClasses: v1beta1AllowedListSpec,
AllowedHostnames: v1beta1AllowedListSpec,
},
ContainerRegistries: &v1beta2AllowedListSpec.AllowedListSpec,
ContainerRegistries: v1beta1AllowedListSpec,
NodeSelector: nodeSelector,
NetworkPolicies: api.NetworkPolicySpec{
NetworkPolicies: capsulev1beta1.NetworkPolicySpec{
Items: networkPolicies,
},
LimitRanges: api.LimitRangesSpec{
LimitRanges: capsulev1beta1.LimitRangesSpec{
Items: limitRanges,
},
ResourceQuota: api.ResourceQuotaSpec{
Scope: api.ResourceQuotaScopeNamespace,
ResourceQuota: capsulev1beta1.ResourceQuotaSpec{
Scope: capsulev1beta1.ResourceQuotaScopeNamespace,
Items: resourceQuotas,
},
AdditionalRoleBindings: []api.AdditionalRoleBindingsSpec{
AdditionalRoleBindings: []capsulev1beta1.AdditionalRoleBindingsSpec{
{
ClusterRoleName: "crds-rolebinding",
Subjects: []rbacv1.Subject{
@@ -267,8 +264,8 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
},
},
},
ImagePullPolicies: []api.ImagePullPolicySpec{"Always", "IfNotPresent"},
PriorityClasses: &api.AllowedListSpec{
ImagePullPolicies: []capsulev1beta1.ImagePullPolicySpec{"Always", "IfNotPresent"},
PriorityClasses: &capsulev1beta1.AllowedListSpec{
Exact: []string{"default"},
Regex: "^tier-.*$",
},
@@ -326,7 +323,7 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
NetworkPolicies: networkPolicies,
LimitRanges: limitRanges,
ResourceQuota: resourceQuotas,
AdditionalRoleBindings: []api.AdditionalRoleBindingsSpec{
AdditionalRoleBindings: []AdditionalRoleBindingsSpec{
{
ClusterRoleName: "crds-rolebinding",
Subjects: []rbacv1.Subject{
@@ -338,8 +335,8 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
},
},
},
ExternalServiceIPs: &api.ExternalServiceIPsSpec{
Allowed: []api.AllowedIP{"192.168.0.1"},
ExternalServiceIPs: &ExternalServiceIPsSpec{
Allowed: []AllowedIP{"192.168.0.1"},
},
},
Status: TenantStatus{

View File

@@ -1,13 +1,11 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package api
package v1alpha1
// +kubebuilder:validation:Pattern="^([0-9]{1,3}.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$"
type AllowedIP string
// +kubebuilder:object:generate=true
type ExternalServiceIPsSpec struct {
Allowed []AllowedIP `json:"allowed"`
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
// Package v1alpha1 contains API Schema definitions for the capsule.clastix.io v1alpha1 API group

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1

View File

@@ -1,7 +1,11 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package tenant
package v1alpha1
import (
"fmt"
)
const (
AvailableIngressClassesAnnotation = "capsule.clastix.io/ingress-classes"
@@ -11,3 +15,11 @@ const (
AllowedRegistriesAnnotation = "capsule.clastix.io/allowed-registries"
AllowedRegistriesRegexpAnnotation = "capsule.clastix.io/allowed-registries-regexp"
)
func UsedQuotaFor(resource fmt.Stringer) string {
return "quota.capsule.clastix.io/used-" + resource.String()
}
func HardQuotaFor(resource fmt.Stringer) string {
return "quota.capsule.clastix.io/hard-" + resource.String()
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
@@ -9,16 +9,24 @@ import (
corev1 "k8s.io/api/core/v1"
)
func (in *Tenant) IsFull() bool {
func (t *Tenant) IsCordoned() bool {
if v, ok := t.Labels["capsule.clastix.io/cordon"]; ok && v == "enabled" {
return true
}
return false
}
func (t *Tenant) IsFull() bool {
// we don't have limits on assigned Namespaces
if in.Spec.NamespaceQuota == nil {
if t.Spec.NamespaceQuota == nil {
return false
}
return len(in.Status.Namespaces) >= int(*in.Spec.NamespaceQuota)
return len(t.Status.Namespaces) >= int(*t.Spec.NamespaceQuota)
}
func (in *Tenant) AssignNamespaces(namespaces []corev1.Namespace) {
func (t *Tenant) AssignNamespaces(namespaces []corev1.Namespace) {
var l []string
for _, ns := range namespaces {
@@ -29,6 +37,6 @@ func (in *Tenant) AssignNamespaces(namespaces []corev1.Namespace) {
sort.Strings(l)
in.Status.Namespaces = l
in.Status.Size = uint(len(l))
t.Status.Namespaces = l
t.Status.Size = uint(len(l))
}

View File

@@ -1,7 +1,7 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta2
package v1alpha1
import (
"fmt"
@@ -9,10 +9,10 @@ import (
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
func GetTypeLabel(t metav1.Object) (label string, err error) {
func GetTypeLabel(t runtime.Object) (label string, err error) {
switch v := t.(type) {
case *Tenant:
return "capsule.clastix.io/tenant", nil

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
@@ -7,28 +7,26 @@ import (
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/projectcapsule/capsule/pkg/api"
)
// TenantSpec defines the desired state of Tenant.
type TenantSpec struct {
Owner OwnerSpec `json:"owner"`
// +kubebuilder:validation:Minimum=1
//+kubebuilder:validation:Minimum=1
NamespaceQuota *int32 `json:"namespaceQuota,omitempty"`
NamespacesMetadata *AdditionalMetadata `json:"namespacesMetadata,omitempty"`
ServicesMetadata *AdditionalMetadata `json:"servicesMetadata,omitempty"`
StorageClasses *api.AllowedListSpec `json:"storageClasses,omitempty"`
IngressClasses *api.AllowedListSpec `json:"ingressClasses,omitempty"`
IngressHostnames *api.AllowedListSpec `json:"ingressHostnames,omitempty"`
ContainerRegistries *api.AllowedListSpec `json:"containerRegistries,omitempty"`
NamespacesMetadata *AdditionalMetadataSpec `json:"namespacesMetadata,omitempty"`
ServicesMetadata *AdditionalMetadataSpec `json:"servicesMetadata,omitempty"`
StorageClasses *AllowedListSpec `json:"storageClasses,omitempty"`
IngressClasses *AllowedListSpec `json:"ingressClasses,omitempty"`
IngressHostnames *AllowedListSpec `json:"ingressHostnames,omitempty"`
ContainerRegistries *AllowedListSpec `json:"containerRegistries,omitempty"`
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
NetworkPolicies []networkingv1.NetworkPolicySpec `json:"networkPolicies,omitempty"`
LimitRanges []corev1.LimitRangeSpec `json:"limitRanges,omitempty"`
ResourceQuota []corev1.ResourceQuotaSpec `json:"resourceQuotas,omitempty"`
AdditionalRoleBindings []api.AdditionalRoleBindingsSpec `json:"additionalRoleBindings,omitempty"`
ExternalServiceIPs *api.ExternalServiceIPsSpec `json:"externalServiceIPs,omitempty"`
AdditionalRoleBindings []AdditionalRoleBindingsSpec `json:"additionalRoleBindings,omitempty"`
ExternalServiceIPs *ExternalServiceIPsSpec `json:"externalServiceIPs,omitempty"`
}
// TenantStatus defines the observed state of Tenant.
@@ -46,7 +44,6 @@ type TenantStatus struct {
// +kubebuilder:printcolumn:name="Owner kind",type="string",JSONPath=".spec.owner.kind",description="The assigned Tenant owner kind"
// +kubebuilder:printcolumn:name="Node selector",type="string",JSONPath=".spec.nodeSelector",description="Node Selector applied to Pods"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age"
// +kubebuilder:deprecatedversion:warning="This version is going to be dropped in the upcoming version of Capsule; please, migrate to v1beta2 version."
// Tenant is the Schema for the tenants API.
type Tenant struct {

View File

@@ -1,21 +1,21 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import (
"os"
"io/ioutil"
ctrl "sigs.k8s.io/controller-runtime"
)
func (in *Tenant) SetupWebhookWithManager(mgr ctrl.Manager) error {
certData, _ := os.ReadFile("/tmp/k8s-webhook-server/serving-certs/tls.crt")
func (t *Tenant) SetupWebhookWithManager(mgr ctrl.Manager) error {
certData, _ := ioutil.ReadFile("/tmp/k8s-webhook-server/serving-certs/tls.crt")
if len(certData) == 0 {
return nil
}
return ctrl.NewWebhookManagedBy(mgr).
For(in).
For(t).
Complete()
}

View File

@@ -1,7 +1,7 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
// Code generated by controller-gen. DO NOT EDIT.
@@ -9,24 +9,24 @@
package v1alpha1
import (
"github.com/projectcapsule/capsule/pkg/api"
corev1 "k8s.io/api/core/v1"
"k8s.io/api/networking/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
networkingv1 "k8s.io/api/networking/v1"
"k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AdditionalMetadata) DeepCopyInto(out *AdditionalMetadata) {
func (in *AdditionalMetadataSpec) DeepCopyInto(out *AdditionalMetadataSpec) {
*out = *in
if in.Labels != nil {
in, out := &in.Labels, &out.Labels
if in.AdditionalLabels != nil {
in, out := &in.AdditionalLabels, &out.AdditionalLabels
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Annotations != nil {
in, out := &in.Annotations, &out.Annotations
if in.AdditionalAnnotations != nil {
in, out := &in.AdditionalAnnotations, &out.AdditionalAnnotations
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
@@ -34,12 +34,52 @@ func (in *AdditionalMetadata) DeepCopyInto(out *AdditionalMetadata) {
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalMetadata.
func (in *AdditionalMetadata) DeepCopy() *AdditionalMetadata {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalMetadataSpec.
func (in *AdditionalMetadataSpec) DeepCopy() *AdditionalMetadataSpec {
if in == nil {
return nil
}
out := new(AdditionalMetadata)
out := new(AdditionalMetadataSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AdditionalRoleBindingsSpec) DeepCopyInto(out *AdditionalRoleBindingsSpec) {
*out = *in
if in.Subjects != nil {
in, out := &in.Subjects, &out.Subjects
*out = make([]v1.Subject, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalRoleBindingsSpec.
func (in *AdditionalRoleBindingsSpec) DeepCopy() *AdditionalRoleBindingsSpec {
if in == nil {
return nil
}
out := new(AdditionalRoleBindingsSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AllowedListSpec) DeepCopyInto(out *AllowedListSpec) {
*out = *in
if in.Exact != nil {
in, out := &in.Exact, &out.Exact
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AllowedListSpec.
func (in *AllowedListSpec) DeepCopy() *AllowedListSpec {
if in == nil {
return nil
}
out := new(AllowedListSpec)
in.DeepCopyInto(out)
return out
}
@@ -122,6 +162,26 @@ func (in *CapsuleConfigurationSpec) DeepCopy() *CapsuleConfigurationSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ExternalServiceIPsSpec) DeepCopyInto(out *ExternalServiceIPsSpec) {
*out = *in
if in.Allowed != nil {
in, out := &in.Allowed, &out.Allowed
*out = make([]AllowedIP, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalServiceIPsSpec.
func (in *ExternalServiceIPsSpec) DeepCopy() *ExternalServiceIPsSpec {
if in == nil {
return nil
}
out := new(ExternalServiceIPsSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OwnerSpec) DeepCopyInto(out *OwnerSpec) {
*out = *in
@@ -207,32 +267,32 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
}
if in.NamespacesMetadata != nil {
in, out := &in.NamespacesMetadata, &out.NamespacesMetadata
*out = new(AdditionalMetadata)
*out = new(AdditionalMetadataSpec)
(*in).DeepCopyInto(*out)
}
if in.ServicesMetadata != nil {
in, out := &in.ServicesMetadata, &out.ServicesMetadata
*out = new(AdditionalMetadata)
*out = new(AdditionalMetadataSpec)
(*in).DeepCopyInto(*out)
}
if in.StorageClasses != nil {
in, out := &in.StorageClasses, &out.StorageClasses
*out = new(api.AllowedListSpec)
*out = new(AllowedListSpec)
(*in).DeepCopyInto(*out)
}
if in.IngressClasses != nil {
in, out := &in.IngressClasses, &out.IngressClasses
*out = new(api.AllowedListSpec)
*out = new(AllowedListSpec)
(*in).DeepCopyInto(*out)
}
if in.IngressHostnames != nil {
in, out := &in.IngressHostnames, &out.IngressHostnames
*out = new(api.AllowedListSpec)
*out = new(AllowedListSpec)
(*in).DeepCopyInto(*out)
}
if in.ContainerRegistries != nil {
in, out := &in.ContainerRegistries, &out.ContainerRegistries
*out = new(api.AllowedListSpec)
*out = new(AllowedListSpec)
(*in).DeepCopyInto(*out)
}
if in.NodeSelector != nil {
@@ -244,7 +304,7 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
}
if in.NetworkPolicies != nil {
in, out := &in.NetworkPolicies, &out.NetworkPolicies
*out = make([]v1.NetworkPolicySpec, len(*in))
*out = make([]networkingv1.NetworkPolicySpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
@@ -265,14 +325,14 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
}
if in.AdditionalRoleBindings != nil {
in, out := &in.AdditionalRoleBindings, &out.AdditionalRoleBindings
*out = make([]api.AdditionalRoleBindingsSpec, len(*in))
*out = make([]AdditionalRoleBindingsSpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.ExternalServiceIPs != nil {
in, out := &in.ExternalServiceIPs, &out.ExternalServiceIPs
*out = new(api.ExternalServiceIPsSpec)
*out = new(ExternalServiceIPsSpec)
(*in).DeepCopyInto(*out)
}
}

View File

@@ -1,9 +1,7 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package api
// +kubebuilder:object:generate=true
package v1beta1
type AdditionalMetadataSpec struct {
Labels map[string]string `json:"labels,omitempty"`

View File

@@ -1,12 +1,10 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package api
package v1beta1
import rbacv1 "k8s.io/api/rbac/v1"
// +kubebuilder:object:generate=true
type AdditionalRoleBindingsSpec struct {
ClusterRoleName string `json:"clusterRoleName"`
// kubebuilder:validation:Minimum=1

View File

@@ -0,0 +1,37 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
//nolint:dupl
package v1beta1
import (
"regexp"
"sort"
"strings"
)
type AllowedListSpec struct {
Exact []string `json:"allowed,omitempty"`
Regex string `json:"allowedRegex,omitempty"`
}
func (in *AllowedListSpec) ExactMatch(value string) (ok bool) {
if len(in.Exact) > 0 {
sort.SliceStable(in.Exact, func(i, j int) bool {
return strings.ToLower(in.Exact[i]) < strings.ToLower(in.Exact[j])
})
i := sort.SearchStrings(in.Exact, value)
ok = i < len(in.Exact) && in.Exact[i] == value
}
return
}
func (in AllowedListSpec) RegexMatch(value string) (ok bool) {
if len(in.Regex) > 0 {
ok = regexp.MustCompile(in.Regex).MatchString(value)
}
return
}

View File

@@ -1,8 +1,7 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
//nolint:dupl
package api
package v1beta1
import (
"testing"

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1

View File

@@ -1,14 +1,14 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
const (
DenyWildcard = "capsule.clastix.io/deny-wildcard"
denyWildcard = "capsule.clastix.io/deny-wildcard"
)
func (in *Tenant) IsWildcardDenied() bool {
if v, ok := in.Annotations[DenyWildcard]; ok && v == "true" {
func (t *Tenant) IsWildcardDenied() bool {
if v, ok := t.Annotations[denyWildcard]; ok && v == "true" {
return true
}

View File

@@ -1,7 +1,7 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package api
//nolint:dupl
package v1beta1
import (
"regexp"
@@ -9,14 +9,12 @@ import (
"strings"
)
// +kubebuilder:object:generate=true
type ForbiddenListSpec struct {
Exact []string `json:"denied,omitempty"`
Regex string `json:"deniedRegex,omitempty"`
}
func (in ForbiddenListSpec) ExactMatch(value string) (ok bool) {
func (in *ForbiddenListSpec) ExactMatch(value string) (ok bool) {
if len(in.Exact) > 0 {
sort.SliceStable(in.Exact, func(i, j int) bool {
return strings.ToLower(in.Exact[i]) < strings.ToLower(in.Exact[j])

View File

@@ -1,8 +1,7 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
//
//nolint:dupl
package api
package v1beta1
import (
"testing"

View File

@@ -1,9 +1,9 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
// Package v1beta1 contains API Schema definitions for the capsule v1beta1 API group
// +kubebuilder:object:generate=true
// +groupName=capsule.clastix.io
//+kubebuilder:object:generate=true
//+groupName=capsule.clastix.io
package v1beta1
import (

View File

@@ -1,7 +1,7 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package api
package v1beta1
const (
HostnameCollisionScopeCluster HostnameCollisionScope = "Cluster"

View File

@@ -1,7 +1,7 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package api
package v1beta1
// +kubebuilder:validation:Enum=Always;Never;IfNotPresent
type ImagePullPolicySpec string

View File

@@ -1,15 +1,11 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
import (
"github.com/projectcapsule/capsule/pkg/api"
)
type IngressOptions struct {
// Specifies the allowed IngressClasses assigned to the Tenant. Capsule assures that all Ingress resources created in the Tenant can use only one of the allowed IngressClasses. Optional.
AllowedClasses *api.AllowedListSpec `json:"allowedClasses,omitempty"`
AllowedClasses *AllowedListSpec `json:"allowedClasses,omitempty"`
// Defines the scope of hostname collision check performed when Tenant Owners create Ingress with allowed hostnames.
//
//
@@ -22,7 +18,7 @@ type IngressOptions struct {
//
// Optional.
// +kubebuilder:default=Disabled
HostnameCollisionScope api.HostnameCollisionScope `json:"hostnameCollisionScope,omitempty"`
HostnameCollisionScope HostnameCollisionScope `json:"hostnameCollisionScope,omitempty"`
// Specifies the allowed hostnames in Ingresses for the given Tenant. Capsule assures that all Ingress resources created in the Tenant can use only one of the allowed hostnames. Optional.
AllowedHostnames *api.AllowedListSpec `json:"allowedHostnames,omitempty"`
AllowedHostnames *AllowedListSpec `json:"allowedHostnames,omitempty"`
}

View File

@@ -1,12 +1,10 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package api
package v1beta1
import corev1 "k8s.io/api/core/v1"
// +kubebuilder:object:generate=true
type LimitRangesSpec struct {
Items []corev1.LimitRangeSpec `json:"items,omitempty"`
}

View File

@@ -1,64 +1,57 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package v1beta1
import (
"strings"
"github.com/projectcapsule/capsule/pkg/api"
)
import "strings"
type NamespaceOptions struct {
// +kubebuilder:validation:Minimum=1
//+kubebuilder:validation:Minimum=1
// Specifies the maximum number of namespaces allowed for that Tenant. Once the namespace quota assigned to the Tenant has been reached, the Tenant owner cannot create further namespaces. Optional.
Quota *int32 `json:"quota,omitempty"`
// Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant. Optional.
AdditionalMetadata *api.AdditionalMetadataSpec `json:"additionalMetadata,omitempty"`
AdditionalMetadata *AdditionalMetadataSpec `json:"additionalMetadata,omitempty"`
}
func (in *Tenant) hasForbiddenNamespaceLabelsAnnotations() bool {
if _, ok := in.Annotations[api.ForbiddenNamespaceLabelsAnnotation]; ok {
func (t *Tenant) hasForbiddenNamespaceLabelsAnnotations() bool {
if _, ok := t.Annotations[ForbiddenNamespaceLabelsAnnotation]; ok {
return true
}
if _, ok := in.Annotations[api.ForbiddenNamespaceLabelsRegexpAnnotation]; ok {
if _, ok := t.Annotations[ForbiddenNamespaceLabelsRegexpAnnotation]; ok {
return true
}
return false
}
func (in *Tenant) hasForbiddenNamespaceAnnotationsAnnotations() bool {
if _, ok := in.Annotations[api.ForbiddenNamespaceAnnotationsAnnotation]; ok {
func (t *Tenant) hasForbiddenNamespaceAnnotationsAnnotations() bool {
if _, ok := t.Annotations[ForbiddenNamespaceAnnotationsAnnotation]; ok {
return true
}
if _, ok := in.Annotations[api.ForbiddenNamespaceAnnotationsRegexpAnnotation]; ok {
if _, ok := t.Annotations[ForbiddenNamespaceAnnotationsRegexpAnnotation]; ok {
return true
}
return false
}
func (in *Tenant) ForbiddenUserNamespaceLabels() *api.ForbiddenListSpec {
if !in.hasForbiddenNamespaceLabelsAnnotations() {
func (t *Tenant) ForbiddenUserNamespaceLabels() *ForbiddenListSpec {
if !t.hasForbiddenNamespaceLabelsAnnotations() {
return nil
}
return &api.ForbiddenListSpec{
Exact: strings.Split(in.Annotations[api.ForbiddenNamespaceLabelsAnnotation], ","),
Regex: in.Annotations[api.ForbiddenNamespaceLabelsRegexpAnnotation],
return &ForbiddenListSpec{
Exact: strings.Split(t.Annotations[ForbiddenNamespaceLabelsAnnotation], ","),
Regex: t.Annotations[ForbiddenNamespaceLabelsRegexpAnnotation],
}
}
func (in *Tenant) ForbiddenUserNamespaceAnnotations() *api.ForbiddenListSpec {
if !in.hasForbiddenNamespaceAnnotationsAnnotations() {
func (t *Tenant) ForbiddenUserNamespaceAnnotations() *ForbiddenListSpec {
if !t.hasForbiddenNamespaceAnnotationsAnnotations() {
return nil
}
return &api.ForbiddenListSpec{
Exact: strings.Split(in.Annotations[api.ForbiddenNamespaceAnnotationsAnnotation], ","),
Regex: in.Annotations[api.ForbiddenNamespaceAnnotationsRegexpAnnotation],
return &ForbiddenListSpec{
Exact: strings.Split(t.Annotations[ForbiddenNamespaceAnnotationsAnnotation], ","),
Regex: t.Annotations[ForbiddenNamespaceAnnotationsRegexpAnnotation],
}
}

View File

@@ -1,14 +1,12 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package api
package v1beta1
import (
networkingv1 "k8s.io/api/networking/v1"
)
// +kubebuilder:object:generate=true
type NetworkPolicySpec struct {
Items []networkingv1.NetworkPolicySpec `json:"items,omitempty"`
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1

View File

@@ -1,6 +1,3 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package v1beta1
import (
@@ -9,14 +6,14 @@ import (
type OwnerListSpec []OwnerSpec
func (in OwnerListSpec) FindOwner(name string, kind OwnerKind) (owner OwnerSpec) {
sort.Sort(ByKindAndName(in))
i := sort.Search(len(in), func(i int) bool {
return in[i].Kind >= kind && in[i].Name >= name
func (o OwnerListSpec) FindOwner(name string, kind OwnerKind) (owner OwnerSpec) {
sort.Sort(ByKindAndName(o))
i := sort.Search(len(o), func(i int) bool {
return o[i].Kind >= kind && o[i].Name >= name
})
if i < len(in) && in[i].Kind == kind && in[i].Name == name {
return in[i]
if i < len(o) && o[i].Kind == kind && o[i].Name == name {
return o[i]
}
return
@@ -24,18 +21,18 @@ func (in OwnerListSpec) FindOwner(name string, kind OwnerKind) (owner OwnerSpec)
type ByKindAndName OwnerListSpec
func (in ByKindAndName) Len() int {
return len(in)
func (b ByKindAndName) Len() int {
return len(b)
}
func (in ByKindAndName) Less(i, j int) bool {
if in[i].Kind.String() != in[j].Kind.String() {
return in[i].Kind.String() < in[j].Kind.String()
func (b ByKindAndName) Less(i, j int) bool {
if b[i].Kind.String() != b[j].Kind.String() {
return b[i].Kind.String() < b[j].Kind.String()
}
return in[i].Name < in[j].Name
return b[i].Name < b[j].Name
}
func (in ByKindAndName) Swap(i, j int) {
in[i], in[j] = in[j], in[i]
func (b ByKindAndName) Swap(i, j int) {
b[i], b[j] = b[j], b[i]
}

View File

@@ -1,6 +1,3 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package v1beta1
import (

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
@@ -19,7 +19,7 @@ const (
// 2. the overall length of the annotation key that is exceeding 63 characters
// For emails, the symbol @ can be replaced with the placeholder __AT__.
// For the latter one, the index of the owner can be used to force the retrieval.
func (in *OwnerSpec) GetRoles(tenant Tenant, index int) []string {
func (in OwnerSpec) GetRoles(tenant Tenant, index int) []string {
for key, value := range tenant.GetAnnotations() {
if !strings.HasPrefix(key, fmt.Sprintf("%s/", ClusterRoleNamesAnnotation)) {
continue
@@ -38,10 +38,36 @@ func (in *OwnerSpec) GetRoles(tenant Tenant, index int) []string {
}
}
return []string{"admin", "capsule-namespace-deleter"}
roles := []string{"admin", "capsule-namespace-deleter"}
if tenant.Spec.GitOpsReady {
roles = append(roles, in.getGitOpsRoles(tenant)...)
}
return roles
}
func (in *OwnerSpec) convertMap() map[string]string {
func (in OwnerSpec) GetClusterRoles(tenant Tenant) []string {
if tenant.Spec.GitOpsReady {
return in.getGitOpsClusterRoles(tenant)
}
return []string{}
}
func (in OwnerSpec) getGitOpsClusterRoles(tenant Tenant) []string {
return []string{
"capsule-tenant-impersonator-" + tenant.Name + "-" + in.Name,
}
}
func (in OwnerSpec) getGitOpsRoles(tenant Tenant) []string {
return []string{
"cluster-admin",
}
}
func (in OwnerSpec) convertMap() map[string]string {
return map[string]string{
"__AT__": "@",
}

View File

@@ -1,7 +1,7 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package api
package v1beta1
import corev1 "k8s.io/api/core/v1"
@@ -13,8 +13,6 @@ const (
ResourceQuotaScopeNamespace ResourceQuotaScope = "Namespace"
)
// +kubebuilder:object:generate=true
type ResourceQuotaSpec struct {
// +kubebuilder:default=Tenant
// Define if the Resource Budget should compute resource across all Namespaces in the Tenant or individually per cluster. Default is Tenant

View File

@@ -0,0 +1,11 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
// +kubebuilder:validation:Pattern="^([0-9]{1,3}.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$"
type AllowedIP string
type ExternalServiceIPsSpec struct {
Allowed []AllowedIP `json:"allowed"`
}

View File

@@ -1,16 +1,16 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
type AllowedServices struct {
// +kubebuilder:default=true
//+kubebuilder:default=true
// Specifies if NodePort service type resources are allowed for the Tenant. Default is true. Optional.
NodePort *bool `json:"nodePort,omitempty"`
// +kubebuilder:default=true
//+kubebuilder:default=true
// Specifies if ExternalName service type resources are allowed for the Tenant. Default is true. Optional.
ExternalName *bool `json:"externalName,omitempty"`
// +kubebuilder:default=true
//+kubebuilder:default=true
// Specifies if LoadBalancer service type resources are allowed for the Tenant. Default is true. Optional.
LoadBalancer *bool `json:"loadBalancer,omitempty"`
}

View File

@@ -1,17 +1,13 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
import (
"github.com/projectcapsule/capsule/pkg/api"
)
type ServiceOptions struct {
// Specifies additional labels and annotations the Capsule operator places on any Service resource in the Tenant. Optional.
AdditionalMetadata *api.AdditionalMetadataSpec `json:"additionalMetadata,omitempty"`
AdditionalMetadata *AdditionalMetadataSpec `json:"additionalMetadata,omitempty"`
// Block or deny certain type of Services. Optional.
AllowedServices *api.AllowedServices `json:"allowedServices,omitempty"`
AllowedServices *AllowedServices `json:"allowedServices,omitempty"`
// Specifies the external IPs that can be used in Services with type ClusterIP. An empty list means no IPs are allowed. Optional.
ExternalServiceIPs *api.ExternalServiceIPsSpec `json:"externalIPs,omitempty"`
ExternalServiceIPs *ExternalServiceIPsSpec `json:"externalIPs,omitempty"`
}

View File

@@ -0,0 +1,31 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
import (
"fmt"
"strings"
)
const (
AvailableIngressClassesAnnotation = "capsule.clastix.io/ingress-classes"
AvailableIngressClassesRegexpAnnotation = "capsule.clastix.io/ingress-classes-regexp"
AvailableStorageClassesAnnotation = "capsule.clastix.io/storage-classes"
AvailableStorageClassesRegexpAnnotation = "capsule.clastix.io/storage-classes-regexp"
AllowedRegistriesAnnotation = "capsule.clastix.io/allowed-registries"
AllowedRegistriesRegexpAnnotation = "capsule.clastix.io/allowed-registries-regexp"
ForbiddenNamespaceLabelsAnnotation = "capsule.clastix.io/forbidden-namespace-labels"
ForbiddenNamespaceLabelsRegexpAnnotation = "capsule.clastix.io/forbidden-namespace-labels-regexp"
ForbiddenNamespaceAnnotationsAnnotation = "capsule.clastix.io/forbidden-namespace-annotations"
ForbiddenNamespaceAnnotationsRegexpAnnotation = "capsule.clastix.io/forbidden-namespace-annotations-regexp"
ProtectedTenantAnnotation = "capsule.clastix.io/protected"
)
func UsedQuotaFor(resource fmt.Stringer) string {
return "quota.capsule.clastix.io/used-" + strings.ReplaceAll(resource.String(), "/", "_")
}
func HardQuotaFor(resource fmt.Stringer) string {
return "quota.capsule.clastix.io/hard-" + strings.ReplaceAll(resource.String(), "/", "_")
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
@@ -9,24 +9,24 @@ import (
corev1 "k8s.io/api/core/v1"
)
func (in *Tenant) IsCordoned() bool {
if v, ok := in.Labels["capsule.clastix.io/cordon"]; ok && v == "enabled" {
func (t *Tenant) IsCordoned() bool {
if v, ok := t.Labels["capsule.clastix.io/cordon"]; ok && v == "enabled" {
return true
}
return false
}
func (in *Tenant) IsFull() bool {
func (t *Tenant) IsFull() bool {
// we don't have limits on assigned Namespaces
if in.Spec.NamespaceOptions == nil || in.Spec.NamespaceOptions.Quota == nil {
if t.Spec.NamespaceOptions == nil || t.Spec.NamespaceOptions.Quota == nil {
return false
}
return len(in.Status.Namespaces) >= int(*in.Spec.NamespaceOptions.Quota)
return len(t.Status.Namespaces) >= int(*t.Spec.NamespaceOptions.Quota)
}
func (in *Tenant) AssignNamespaces(namespaces []corev1.Namespace) {
func (t *Tenant) AssignNamespaces(namespaces []corev1.Namespace) {
var l []string
for _, ns := range namespaces {
@@ -37,10 +37,10 @@ func (in *Tenant) AssignNamespaces(namespaces []corev1.Namespace) {
sort.Strings(l)
in.Status.Namespaces = l
in.Status.Size = uint(len(l))
t.Status.Namespaces = l
t.Status.Size = uint(len(l))
}
func (in *Tenant) GetOwnerProxySettings(name string, kind OwnerKind) []ProxySettings {
return in.Spec.Owners.FindOwner(name, kind).ProxyOperations
func (t *Tenant) GetOwnerProxySettings(name string, kind OwnerKind) []ProxySettings {
return t.Spec.Owners.FindOwner(name, kind).ProxyOperations
}

View File

@@ -1,7 +1,7 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package utils
package v1beta1
import (
"fmt"
@@ -10,15 +10,11 @@ import (
networkingv1 "k8s.io/api/networking/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/runtime"
"github.com/projectcapsule/capsule/api/v1alpha1"
"github.com/projectcapsule/capsule/api/v1beta1"
"github.com/projectcapsule/capsule/api/v1beta2"
)
func GetTypeLabel(t runtime.Object) (label string, err error) {
switch v := t.(type) {
case *v1alpha1.Tenant, *v1beta1.Tenant, *v1beta2.Tenant:
case *Tenant:
return "capsule.clastix.io/tenant", nil
case *corev1.LimitRange:
return "capsule.clastix.io/limit-range", nil
@@ -28,6 +24,8 @@ func GetTypeLabel(t runtime.Object) (label string, err error) {
return "capsule.clastix.io/resource-quota", nil
case *rbacv1.RoleBinding:
return "capsule.clastix.io/role-binding", nil
case *rbacv1.ClusterRoleBinding:
return "capsule.clastix.io/cluster-role-binding", nil
default:
err = fmt.Errorf("type %T is not mapped as Capsule label recognized", v)
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
@@ -13,7 +13,7 @@ const (
// Returns the observed state of the Tenant.
type TenantStatus struct {
// +kubebuilder:default=Active
//+kubebuilder:default=Active
// The operational state of the Tenant. Possible values are "Active", "Cordoned".
State tenantState `json:"state"`
// How many namespaces are assigned to the Tenant.

View File

@@ -1,12 +1,10 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/projectcapsule/capsule/pkg/api"
)
// TenantSpec defines the desired state of Tenant.
@@ -16,31 +14,34 @@ type TenantSpec struct {
// Specifies options for the Namespaces, such as additional metadata or maximum number of namespaces allowed for that Tenant. Once the namespace quota assigned to the Tenant has been reached, the Tenant owner cannot create further namespaces. Optional.
NamespaceOptions *NamespaceOptions `json:"namespaceOptions,omitempty"`
// Specifies options for the Service, such as additional metadata or block of certain type of Services. Optional.
ServiceOptions *api.ServiceOptions `json:"serviceOptions,omitempty"`
ServiceOptions *ServiceOptions `json:"serviceOptions,omitempty"`
// Specifies the allowed StorageClasses assigned to the Tenant. Capsule assures that all PersistentVolumeClaim resources created in the Tenant can use only one of the allowed StorageClasses. Optional.
StorageClasses *api.AllowedListSpec `json:"storageClasses,omitempty"`
StorageClasses *AllowedListSpec `json:"storageClasses,omitempty"`
// Specifies options for the Ingress resources, such as allowed hostnames and IngressClass. Optional.
IngressOptions IngressOptions `json:"ingressOptions,omitempty"`
// Specifies the trusted Image Registries assigned to the Tenant. Capsule assures that all Pods resources created in the Tenant can use only one of the allowed trusted registries. Optional.
ContainerRegistries *api.AllowedListSpec `json:"containerRegistries,omitempty"`
ContainerRegistries *AllowedListSpec `json:"containerRegistries,omitempty"`
// Specifies the label to control the placement of pods on a given pool of worker nodes. All namespaces created within the Tenant will have the node selector annotation. This annotation tells the Kubernetes scheduler to place pods on the nodes having the selector label. Optional.
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
// Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
NetworkPolicies api.NetworkPolicySpec `json:"networkPolicies,omitempty"`
NetworkPolicies NetworkPolicySpec `json:"networkPolicies,omitempty"`
// Specifies the resource min/max usage restrictions to the Tenant. The assigned values are inherited by any namespace created in the Tenant. Optional.
LimitRanges api.LimitRangesSpec `json:"limitRanges,omitempty"`
LimitRanges LimitRangesSpec `json:"limitRanges,omitempty"`
// Specifies a list of ResourceQuota resources assigned to the Tenant. The assigned values are inherited by any namespace created in the Tenant. The Capsule operator aggregates ResourceQuota at Tenant level, so that the hard quota is never crossed for the given Tenant. This permits the Tenant owner to consume resources in the Tenant regardless of the namespace. Optional.
ResourceQuota api.ResourceQuotaSpec `json:"resourceQuotas,omitempty"`
ResourceQuota ResourceQuotaSpec `json:"resourceQuotas,omitempty"`
// Specifies additional RoleBindings assigned to the Tenant. Capsule will ensure that all namespaces in the Tenant always contain the RoleBinding for the given ClusterRole. Optional.
AdditionalRoleBindings []api.AdditionalRoleBindingsSpec `json:"additionalRoleBindings,omitempty"`
AdditionalRoleBindings []AdditionalRoleBindingsSpec `json:"additionalRoleBindings,omitempty"`
// Specify the allowed values for the imagePullPolicies option in Pod resources. Capsule assures that all Pod resources created in the Tenant can use only one of the allowed policy. Optional.
ImagePullPolicies []api.ImagePullPolicySpec `json:"imagePullPolicies,omitempty"`
ImagePullPolicies []ImagePullPolicySpec `json:"imagePullPolicies,omitempty"`
// Specifies the allowed priorityClasses assigned to the Tenant. Capsule assures that all Pods resources created in the Tenant can use only one of the allowed PriorityClasses. Optional.
PriorityClasses *api.AllowedListSpec `json:"priorityClasses,omitempty"`
PriorityClasses *AllowedListSpec `json:"priorityClasses,omitempty"`
// Configured RBAC for machine owners tailored for GitOps controllers.
GitOpsReady bool `json:"gitOpsReady,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:storageversion
// +kubebuilder:resource:scope=Cluster,shortName=tnt
// +kubebuilder:printcolumn:name="State",type="string",JSONPath=".status.state",description="The actual state of the Tenant"
// +kubebuilder:printcolumn:name="Namespace quota",type="integer",JSONPath=".spec.namespaceOptions.quota",description="The max amount of Namespaces can be created"
@@ -57,9 +58,9 @@ type Tenant struct {
Status TenantStatus `json:"status,omitempty"`
}
func (in *Tenant) Hub() {}
func (t *Tenant) Hub() {}
// +kubebuilder:object:root=true
//+kubebuilder:object:root=true
// TenantList contains a list of Tenant.
type TenantList struct {
@@ -71,11 +72,3 @@ type TenantList struct {
func init() {
SchemeBuilder.Register(&Tenant{}, &TenantList{})
}
func (in *Tenant) GetNamespaces() (res []string) {
res = make([]string, 0, len(in.Status.Namespaces))
res = append(res, in.Status.Namespaces...)
return
}

View File

@@ -1,21 +0,0 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package v1beta1
import (
"os"
ctrl "sigs.k8s.io/controller-runtime"
)
func (in *Tenant) SetupWebhookWithManager(mgr ctrl.Manager) error {
certData, _ := os.ReadFile("/tmp/k8s-webhook-server/serving-certs/tls.crt")
if len(certData) == 0 {
return nil
}
return ctrl.NewWebhookManagedBy(mgr).
For(in).
Complete()
}

View File

@@ -1,7 +1,7 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
// Code generated by controller-gen. DO NOT EDIT.
@@ -9,10 +9,81 @@
package v1beta1
import (
"github.com/projectcapsule/capsule/pkg/api"
runtime "k8s.io/apimachinery/pkg/runtime"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
"k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AdditionalMetadataSpec) DeepCopyInto(out *AdditionalMetadataSpec) {
*out = *in
if in.Labels != nil {
in, out := &in.Labels, &out.Labels
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Annotations != nil {
in, out := &in.Annotations, &out.Annotations
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalMetadataSpec.
func (in *AdditionalMetadataSpec) DeepCopy() *AdditionalMetadataSpec {
if in == nil {
return nil
}
out := new(AdditionalMetadataSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AdditionalRoleBindingsSpec) DeepCopyInto(out *AdditionalRoleBindingsSpec) {
*out = *in
if in.Subjects != nil {
in, out := &in.Subjects, &out.Subjects
*out = make([]v1.Subject, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalRoleBindingsSpec.
func (in *AdditionalRoleBindingsSpec) DeepCopy() *AdditionalRoleBindingsSpec {
if in == nil {
return nil
}
out := new(AdditionalRoleBindingsSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AllowedListSpec) DeepCopyInto(out *AllowedListSpec) {
*out = *in
if in.Exact != nil {
in, out := &in.Exact, &out.Exact
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AllowedListSpec.
func (in *AllowedListSpec) DeepCopy() *AllowedListSpec {
if in == nil {
return nil
}
out := new(AllowedListSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AllowedServices) DeepCopyInto(out *AllowedServices) {
*out = *in
@@ -64,17 +135,57 @@ func (in ByKindAndName) DeepCopy() ByKindAndName {
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ExternalServiceIPsSpec) DeepCopyInto(out *ExternalServiceIPsSpec) {
*out = *in
if in.Allowed != nil {
in, out := &in.Allowed, &out.Allowed
*out = make([]AllowedIP, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalServiceIPsSpec.
func (in *ExternalServiceIPsSpec) DeepCopy() *ExternalServiceIPsSpec {
if in == nil {
return nil
}
out := new(ExternalServiceIPsSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ForbiddenListSpec) DeepCopyInto(out *ForbiddenListSpec) {
*out = *in
if in.Exact != nil {
in, out := &in.Exact, &out.Exact
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ForbiddenListSpec.
func (in *ForbiddenListSpec) DeepCopy() *ForbiddenListSpec {
if in == nil {
return nil
}
out := new(ForbiddenListSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *IngressOptions) DeepCopyInto(out *IngressOptions) {
*out = *in
if in.AllowedClasses != nil {
in, out := &in.AllowedClasses, &out.AllowedClasses
*out = new(api.AllowedListSpec)
*out = new(AllowedListSpec)
(*in).DeepCopyInto(*out)
}
if in.AllowedHostnames != nil {
in, out := &in.AllowedHostnames, &out.AllowedHostnames
*out = new(api.AllowedListSpec)
*out = new(AllowedListSpec)
(*in).DeepCopyInto(*out)
}
}
@@ -89,6 +200,28 @@ func (in *IngressOptions) DeepCopy() *IngressOptions {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LimitRangesSpec) DeepCopyInto(out *LimitRangesSpec) {
*out = *in
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]corev1.LimitRangeSpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LimitRangesSpec.
func (in *LimitRangesSpec) DeepCopy() *LimitRangesSpec {
if in == nil {
return nil
}
out := new(LimitRangesSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NamespaceOptions) DeepCopyInto(out *NamespaceOptions) {
*out = *in
@@ -99,7 +232,7 @@ func (in *NamespaceOptions) DeepCopyInto(out *NamespaceOptions) {
}
if in.AdditionalMetadata != nil {
in, out := &in.AdditionalMetadata, &out.AdditionalMetadata
*out = new(api.AdditionalMetadataSpec)
*out = new(AdditionalMetadataSpec)
(*in).DeepCopyInto(*out)
}
}
@@ -114,6 +247,28 @@ func (in *NamespaceOptions) DeepCopy() *NamespaceOptions {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NetworkPolicySpec) DeepCopyInto(out *NetworkPolicySpec) {
*out = *in
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]networkingv1.NetworkPolicySpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkPolicySpec.
func (in *NetworkPolicySpec) DeepCopy() *NetworkPolicySpec {
if in == nil {
return nil
}
out := new(NetworkPolicySpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NonLimitedResourceError) DeepCopyInto(out *NonLimitedResourceError) {
*out = *in
@@ -192,22 +347,44 @@ func (in *ProxySettings) DeepCopy() *ProxySettings {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ResourceQuotaSpec) DeepCopyInto(out *ResourceQuotaSpec) {
*out = *in
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]corev1.ResourceQuotaSpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceQuotaSpec.
func (in *ResourceQuotaSpec) DeepCopy() *ResourceQuotaSpec {
if in == nil {
return nil
}
out := new(ResourceQuotaSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ServiceOptions) DeepCopyInto(out *ServiceOptions) {
*out = *in
if in.AdditionalMetadata != nil {
in, out := &in.AdditionalMetadata, &out.AdditionalMetadata
*out = new(api.AdditionalMetadataSpec)
*out = new(AdditionalMetadataSpec)
(*in).DeepCopyInto(*out)
}
if in.AllowedServices != nil {
in, out := &in.AllowedServices, &out.AllowedServices
*out = new(api.AllowedServices)
*out = new(AllowedServices)
(*in).DeepCopyInto(*out)
}
if in.ExternalServiceIPs != nil {
in, out := &in.ExternalServiceIPs, &out.ExternalServiceIPs
*out = new(api.ExternalServiceIPsSpec)
*out = new(ExternalServiceIPsSpec)
(*in).DeepCopyInto(*out)
}
}
@@ -298,18 +475,18 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
}
if in.ServiceOptions != nil {
in, out := &in.ServiceOptions, &out.ServiceOptions
*out = new(api.ServiceOptions)
*out = new(ServiceOptions)
(*in).DeepCopyInto(*out)
}
if in.StorageClasses != nil {
in, out := &in.StorageClasses, &out.StorageClasses
*out = new(api.AllowedListSpec)
*out = new(AllowedListSpec)
(*in).DeepCopyInto(*out)
}
in.IngressOptions.DeepCopyInto(&out.IngressOptions)
if in.ContainerRegistries != nil {
in, out := &in.ContainerRegistries, &out.ContainerRegistries
*out = new(api.AllowedListSpec)
*out = new(AllowedListSpec)
(*in).DeepCopyInto(*out)
}
if in.NodeSelector != nil {
@@ -324,19 +501,19 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
in.ResourceQuota.DeepCopyInto(&out.ResourceQuota)
if in.AdditionalRoleBindings != nil {
in, out := &in.AdditionalRoleBindings, &out.AdditionalRoleBindings
*out = make([]api.AdditionalRoleBindingsSpec, len(*in))
*out = make([]AdditionalRoleBindingsSpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.ImagePullPolicies != nil {
in, out := &in.ImagePullPolicies, &out.ImagePullPolicies
*out = make([]api.ImagePullPolicySpec, len(*in))
*out = make([]ImagePullPolicySpec, len(*in))
copy(*out, *in)
}
if in.PriorityClasses != nil {
in, out := &in.PriorityClasses, &out.PriorityClasses
*out = new(api.AllowedListSpec)
*out = new(AllowedListSpec)
(*in).DeepCopyInto(*out)
}
}

View File

@@ -1,142 +0,0 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package v1beta2
import (
"fmt"
"strconv"
"strings"
"sigs.k8s.io/controller-runtime/pkg/conversion"
capsulev1alpha1 "github.com/projectcapsule/capsule/api/v1alpha1"
)
func (in *CapsuleConfiguration) ConvertTo(raw conversion.Hub) error {
dst, ok := raw.(*capsulev1alpha1.CapsuleConfiguration)
if !ok {
return fmt.Errorf("expected type *capsulev1alpha1.CapsuleConfiguration, got %T", dst)
}
dst.ObjectMeta = in.ObjectMeta
dst.Spec.ProtectedNamespaceRegexpString = in.Spec.ProtectedNamespaceRegexpString
dst.Spec.UserGroups = in.Spec.UserGroups
dst.Spec.ProtectedNamespaceRegexpString = in.Spec.ProtectedNamespaceRegexpString
annotations := dst.GetAnnotations()
if annotations == nil {
annotations = make(map[string]string)
}
if in.Spec.NodeMetadata != nil {
if len(in.Spec.NodeMetadata.ForbiddenLabels.Exact) > 0 {
annotations[capsulev1alpha1.ForbiddenNodeLabelsAnnotation] = strings.Join(in.Spec.NodeMetadata.ForbiddenLabels.Exact, ",")
}
if len(in.Spec.NodeMetadata.ForbiddenLabels.Regex) > 0 {
annotations[capsulev1alpha1.ForbiddenNodeLabelsRegexpAnnotation] = in.Spec.NodeMetadata.ForbiddenLabels.Regex
}
if len(in.Spec.NodeMetadata.ForbiddenAnnotations.Exact) > 0 {
annotations[capsulev1alpha1.ForbiddenNodeAnnotationsAnnotation] = strings.Join(in.Spec.NodeMetadata.ForbiddenAnnotations.Exact, ",")
}
if len(in.Spec.NodeMetadata.ForbiddenAnnotations.Regex) > 0 {
annotations[capsulev1alpha1.ForbiddenNodeAnnotationsRegexpAnnotation] = in.Spec.NodeMetadata.ForbiddenAnnotations.Regex
}
}
annotations[capsulev1alpha1.EnableTLSConfigurationAnnotationName] = fmt.Sprintf("%t", in.Spec.EnableTLSReconciler)
annotations[capsulev1alpha1.TLSSecretNameAnnotation] = in.Spec.CapsuleResources.TLSSecretName
annotations[capsulev1alpha1.MutatingWebhookConfigurationName] = in.Spec.CapsuleResources.MutatingWebhookConfigurationName
annotations[capsulev1alpha1.ValidatingWebhookConfigurationName] = in.Spec.CapsuleResources.ValidatingWebhookConfigurationName
dst.SetAnnotations(annotations)
return nil
}
func (in *CapsuleConfiguration) ConvertFrom(raw conversion.Hub) error {
src, ok := raw.(*capsulev1alpha1.CapsuleConfiguration)
if !ok {
return fmt.Errorf("expected type *capsulev1alpha1.CapsuleConfiguration, got %T", src)
}
in.ObjectMeta = src.ObjectMeta
in.Spec.ProtectedNamespaceRegexpString = src.Spec.ProtectedNamespaceRegexpString
in.Spec.UserGroups = src.Spec.UserGroups
in.Spec.ProtectedNamespaceRegexpString = src.Spec.ProtectedNamespaceRegexpString
annotations := src.GetAnnotations()
if value, found := annotations[capsulev1alpha1.ForbiddenNodeLabelsAnnotation]; found {
if in.Spec.NodeMetadata == nil {
in.Spec.NodeMetadata = &NodeMetadata{}
}
in.Spec.NodeMetadata.ForbiddenLabels.Exact = strings.Split(value, ",")
delete(annotations, capsulev1alpha1.ForbiddenNodeLabelsAnnotation)
}
if value, found := annotations[capsulev1alpha1.ForbiddenNodeLabelsRegexpAnnotation]; found {
if in.Spec.NodeMetadata == nil {
in.Spec.NodeMetadata = &NodeMetadata{}
}
in.Spec.NodeMetadata.ForbiddenLabels.Regex = value
delete(annotations, capsulev1alpha1.ForbiddenNodeLabelsRegexpAnnotation)
}
if value, found := annotations[capsulev1alpha1.ForbiddenNodeAnnotationsAnnotation]; found {
if in.Spec.NodeMetadata == nil {
in.Spec.NodeMetadata = &NodeMetadata{}
}
in.Spec.NodeMetadata.ForbiddenAnnotations.Exact = strings.Split(value, ",")
delete(annotations, capsulev1alpha1.ForbiddenNodeAnnotationsAnnotation)
}
if value, found := annotations[capsulev1alpha1.ForbiddenNodeAnnotationsRegexpAnnotation]; found {
if in.Spec.NodeMetadata == nil {
in.Spec.NodeMetadata = &NodeMetadata{}
}
in.Spec.NodeMetadata.ForbiddenAnnotations.Regex = value
delete(annotations, capsulev1alpha1.ForbiddenNodeAnnotationsRegexpAnnotation)
}
if value, found := annotations[capsulev1alpha1.EnableTLSConfigurationAnnotationName]; found {
v, _ := strconv.ParseBool(value)
in.Spec.EnableTLSReconciler = v
delete(annotations, capsulev1alpha1.EnableTLSConfigurationAnnotationName)
}
if value, found := annotations[capsulev1alpha1.TLSSecretNameAnnotation]; found {
in.Spec.CapsuleResources.TLSSecretName = value
delete(annotations, capsulev1alpha1.TLSSecretNameAnnotation)
}
if value, found := annotations[capsulev1alpha1.MutatingWebhookConfigurationName]; found {
in.Spec.CapsuleResources.MutatingWebhookConfigurationName = value
delete(annotations, capsulev1alpha1.MutatingWebhookConfigurationName)
}
if value, found := annotations[capsulev1alpha1.ValidatingWebhookConfigurationName]; found {
in.Spec.CapsuleResources.ValidatingWebhookConfigurationName = value
delete(annotations, capsulev1alpha1.ValidatingWebhookConfigurationName)
}
in.SetAnnotations(annotations)
return nil
}

View File

@@ -1,79 +0,0 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package v1beta2
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/projectcapsule/capsule/pkg/api"
)
// CapsuleConfigurationSpec defines the Capsule configuration.
type CapsuleConfigurationSpec struct {
// Names of the groups for Capsule users.
// +kubebuilder:default={capsule.clastix.io}
UserGroups []string `json:"userGroups,omitempty"`
// Enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix,
// separated by a dash. This is useful to avoid Namespace name collision in a public CaaS environment.
// +kubebuilder:default=false
ForceTenantPrefix bool `json:"forceTenantPrefix,omitempty"`
// Disallow creation of namespaces, whose name matches this regexp
ProtectedNamespaceRegexpString string `json:"protectedNamespaceRegex,omitempty"`
// Allows to set different name rather than the canonical one for the Capsule configuration objects,
// such as webhook secret or configurations.
// +kubebuilder:default={TLSSecretName:"capsule-tls",mutatingWebhookConfigurationName:"capsule-mutating-webhook-configuration",validatingWebhookConfigurationName:"capsule-validating-webhook-configuration"}
CapsuleResources CapsuleResources `json:"overrides,omitempty"`
// Allows to set the forbidden metadata for the worker nodes that could be patched by a Tenant.
// This applies only if the Tenant has an active NodeSelector, and the Owner have right to patch their nodes.
NodeMetadata *NodeMetadata `json:"nodeMetadata,omitempty"`
// Toggles the TLS reconciler, the controller that is able to generate CA and certificates for the webhooks
// when not using an already provided CA and certificate, or when these are managed externally with Vault, or cert-manager.
// +kubebuilder:default=true
EnableTLSReconciler bool `json:"enableTLSReconciler"` //nolint:tagliatelle
}
type NodeMetadata struct {
// Define the labels that a Tenant Owner cannot set for their nodes.
ForbiddenLabels api.ForbiddenListSpec `json:"forbiddenLabels"`
// Define the annotations that a Tenant Owner cannot set for their nodes.
ForbiddenAnnotations api.ForbiddenListSpec `json:"forbiddenAnnotations"`
}
type CapsuleResources struct {
// Defines the Secret name used for the webhook server.
// Must be in the same Namespace where the Capsule Deployment is deployed.
// +kubebuilder:default=capsule-tls
TLSSecretName string `json:"TLSSecretName"` //nolint:tagliatelle
// Name of the MutatingWebhookConfiguration which contains the dynamic admission controller paths and resources.
// +kubebuilder:default=capsule-mutating-webhook-configuration
MutatingWebhookConfigurationName string `json:"mutatingWebhookConfigurationName"`
// Name of the ValidatingWebhookConfiguration which contains the dynamic admission controller paths and resources.
// +kubebuilder:default=capsule-validating-webhook-configuration
ValidatingWebhookConfigurationName string `json:"validatingWebhookConfigurationName"`
}
// +kubebuilder:object:root=true
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:storageversion
// CapsuleConfiguration is the Schema for the Capsule configuration API.
type CapsuleConfiguration struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec CapsuleConfigurationSpec `json:"spec,omitempty"`
}
// +kubebuilder:object:root=true
// CapsuleConfigurationList contains a list of CapsuleConfiguration.
type CapsuleConfigurationList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []CapsuleConfiguration `json:"items"`
}
func init() {
SchemeBuilder.Register(&CapsuleConfiguration{}, &CapsuleConfigurationList{})
}

View File

@@ -1,59 +0,0 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package v1beta2
import (
"fmt"
"strconv"
)
const (
ResourceQuotaAnnotationPrefix = "quota.resources.capsule.clastix.io"
ResourceUsedAnnotationPrefix = "used.resources.capsule.clastix.io"
)
func UsedAnnotationForResource(kindGroup string) string {
return fmt.Sprintf("%s/%s", ResourceUsedAnnotationPrefix, kindGroup)
}
func LimitAnnotationForResource(kindGroup string) string {
return fmt.Sprintf("%s/%s", ResourceQuotaAnnotationPrefix, kindGroup)
}
func GetUsedResourceFromTenant(tenant Tenant, kindGroup string) (int64, error) {
usedStr, ok := tenant.GetAnnotations()[UsedAnnotationForResource(kindGroup)]
if !ok {
usedStr = "0"
}
used, _ := strconv.ParseInt(usedStr, 10, 10)
return used, nil
}
type NonLimitedResourceError struct {
kindGroup string
}
func NewNonLimitedResourceError(kindGroup string) *NonLimitedResourceError {
return &NonLimitedResourceError{kindGroup: kindGroup}
}
func (n NonLimitedResourceError) Error() string {
return fmt.Sprintf("resource %s is not limited for the current tenant", n.kindGroup)
}
func GetLimitResourceFromTenant(tenant Tenant, kindGroup string) (int64, error) {
limitStr, ok := tenant.GetAnnotations()[LimitAnnotationForResource(kindGroup)]
if !ok {
return 0, NewNonLimitedResourceError(kindGroup)
}
limit, err := strconv.ParseInt(limitStr, 10, 10)
if err != nil {
return 0, fmt.Errorf("resource %s limit cannot be parsed, %w", kindGroup, err)
}
return limit, nil
}

View File

@@ -1,23 +0,0 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
// Package v1beta2 contains API Schema definitions for the capsule v1beta2 API group
// +kubebuilder:object:generate=true
// +groupName=capsule.clastix.io
package v1beta2
import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)
var (
// GroupVersion is group version used to register these objects.
GroupVersion = schema.GroupVersion{Group: "capsule.clastix.io", Version: "v1beta2"}
// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
// AddToScheme adds the types in this group-version to the given scheme.
AddToScheme = SchemeBuilder.AddToScheme
)

View File

@@ -1,33 +0,0 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package v1beta2
import (
"github.com/projectcapsule/capsule/pkg/api"
)
type IngressOptions struct {
// Specifies the allowed IngressClasses assigned to the Tenant.
// Capsule assures that all Ingress resources created in the Tenant can use only one of the allowed IngressClasses.
// A default value can be specified, and all the Ingress resources created will inherit the declared class.
// Optional.
AllowedClasses *api.DefaultAllowedListSpec `json:"allowedClasses,omitempty"`
// Defines the scope of hostname collision check performed when Tenant Owners create Ingress with allowed hostnames.
//
//
// - Cluster: disallow the creation of an Ingress if the pair hostname and path is already used across the Namespaces managed by Capsule.
//
// - Tenant: disallow the creation of an Ingress if the pair hostname and path is already used across the Namespaces of the Tenant.
//
// - Namespace: disallow the creation of an Ingress if the pair hostname and path is already used in the Ingress Namespace.
//
//
// Optional.
// +kubebuilder:default=Disabled
HostnameCollisionScope api.HostnameCollisionScope `json:"hostnameCollisionScope,omitempty"`
// Specifies the allowed hostnames in Ingresses for the given Tenant. Capsule assures that all Ingress resources created in the Tenant can use only one of the allowed hostnames. Optional.
AllowedHostnames *api.AllowedListSpec `json:"allowedHostnames,omitempty"`
// Toggles the ability for Ingress resources created in a Tenant to have a hostname wildcard.
AllowWildcardHostnames bool `json:"allowWildcardHostnames,omitempty"`
}

View File

@@ -1,20 +0,0 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package v1beta2
import (
"github.com/projectcapsule/capsule/pkg/api"
)
type NamespaceOptions struct {
// +kubebuilder:validation:Minimum=1
// Specifies the maximum number of namespaces allowed for that Tenant. Once the namespace quota assigned to the Tenant has been reached, the Tenant owner cannot create further namespaces. Optional.
Quota *int32 `json:"quota,omitempty"`
// Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant. Optional.
AdditionalMetadata *api.AdditionalMetadataSpec `json:"additionalMetadata,omitempty"`
// Define the labels that a Tenant Owner cannot set for their Namespace resources.
ForbiddenLabels api.ForbiddenListSpec `json:"forbiddenLabels,omitempty"`
// Define the annotations that a Tenant Owner cannot set for their Namespace resources.
ForbiddenAnnotations api.ForbiddenListSpec `json:"forbiddenAnnotations,omitempty"`
}

View File

@@ -1,59 +0,0 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package v1beta2
type OwnerSpec struct {
// Kind of tenant owner. Possible values are "User", "Group", and "ServiceAccount"
Kind OwnerKind `json:"kind"`
// Name of tenant owner.
Name string `json:"name"`
// Defines additional cluster-roles for the specific Owner.
// +kubebuilder:default={admin,capsule-namespace-deleter}
ClusterRoles []string `json:"clusterRoles,omitempty"`
// Proxy settings for tenant owner.
ProxyOperations []ProxySettings `json:"proxySettings,omitempty"`
}
// +kubebuilder:validation:Enum=User;Group;ServiceAccount
type OwnerKind string
func (k OwnerKind) String() string {
return string(k)
}
type ProxySettings struct {
Kind ProxyServiceKind `json:"kind"`
Operations []ProxyOperation `json:"operations"`
}
// +kubebuilder:validation:Enum=List;Update;Delete
type ProxyOperation string
func (p ProxyOperation) String() string {
return string(p)
}
// +kubebuilder:validation:Enum=Nodes;StorageClasses;IngressClasses;PriorityClasses;RuntimeClasses;PersistentVolumes
type ProxyServiceKind string
func (p ProxyServiceKind) String() string {
return string(p)
}
const (
NodesProxy ProxyServiceKind = "Nodes"
StorageClassesProxy ProxyServiceKind = "StorageClasses"
IngressClassesProxy ProxyServiceKind = "IngressClasses"
PriorityClassesProxy ProxyServiceKind = "PriorityClasses"
RuntimeClassesProxy ProxyServiceKind = "RuntimeClasses"
PersistentVolumesProxy ProxyServiceKind = "PersistentVolumes"
ListOperation ProxyOperation = "List"
UpdateOperation ProxyOperation = "Update"
DeleteOperation ProxyOperation = "Delete"
UserOwner OwnerKind = "User"
GroupOwner OwnerKind = "Group"
ServiceAccountOwner OwnerKind = "ServiceAccount"
)

View File

@@ -1,41 +0,0 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package v1beta2
import (
"sort"
)
type OwnerListSpec []OwnerSpec
func (o OwnerListSpec) FindOwner(name string, kind OwnerKind) (owner OwnerSpec) {
sort.Sort(ByKindAndName(o))
i := sort.Search(len(o), func(i int) bool {
return o[i].Kind >= kind && o[i].Name >= name
})
if i < len(o) && o[i].Kind == kind && o[i].Name == name {
return o[i]
}
return
}
type ByKindAndName OwnerListSpec
func (b ByKindAndName) Len() int {
return len(b)
}
func (b ByKindAndName) Less(i, j int) bool {
if b[i].Kind.String() != b[j].Kind.String() {
return b[i].Kind.String() < b[j].Kind.String()
}
return b[i].Name < b[j].Name
}
func (b ByKindAndName) Swap(i, j int) {
b[i], b[j] = b[j], b[i]
}

View File

@@ -1,86 +0,0 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package v1beta2
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestOwnerListSpec_FindOwner(t *testing.T) {
bla := OwnerSpec{
Kind: UserOwner,
Name: "bla",
ProxyOperations: []ProxySettings{
{
Kind: IngressClassesProxy,
Operations: []ProxyOperation{"Delete"},
},
},
}
bar := OwnerSpec{
Kind: GroupOwner,
Name: "bar",
ProxyOperations: []ProxySettings{
{
Kind: StorageClassesProxy,
Operations: []ProxyOperation{"Delete"},
},
},
}
baz := OwnerSpec{
Kind: UserOwner,
Name: "baz",
ProxyOperations: []ProxySettings{
{
Kind: StorageClassesProxy,
Operations: []ProxyOperation{"Update"},
},
},
}
fim := OwnerSpec{
Kind: ServiceAccountOwner,
Name: "fim",
ProxyOperations: []ProxySettings{
{
Kind: NodesProxy,
Operations: []ProxyOperation{"List"},
},
},
}
bom := OwnerSpec{
Kind: GroupOwner,
Name: "bom",
ProxyOperations: []ProxySettings{
{
Kind: StorageClassesProxy,
Operations: []ProxyOperation{"Delete"},
},
{
Kind: NodesProxy,
Operations: []ProxyOperation{"Delete"},
},
},
}
qip := OwnerSpec{
Kind: ServiceAccountOwner,
Name: "qip",
ProxyOperations: []ProxySettings{
{
Kind: StorageClassesProxy,
Operations: []ProxyOperation{"List", "Delete"},
},
},
}
owners := OwnerListSpec{bom, qip, bla, bar, baz, fim}
assert.Equal(t, owners.FindOwner("bom", GroupOwner), bom)
assert.Equal(t, owners.FindOwner("qip", ServiceAccountOwner), qip)
assert.Equal(t, owners.FindOwner("bla", UserOwner), bla)
assert.Equal(t, owners.FindOwner("bar", GroupOwner), bar)
assert.Equal(t, owners.FindOwner("baz", UserOwner), baz)
assert.Equal(t, owners.FindOwner("fim", ServiceAccountOwner), fim)
assert.Equal(t, owners.FindOwner("notfound", ServiceAccountOwner), OwnerSpec{})
}

View File

@@ -1,44 +0,0 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package v1beta2
import (
"crypto/md5" //#nosec
"encoding/hex"
"fmt"
)
const (
// Annotation name part must be no more than 63 characters.
maxAnnotationLength = 63
)
func createAnnotation(format string, resource fmt.Stringer) (string, error) {
suffix := resource.String()
hash := md5.Sum([]byte(resource.String())) //#nosec
hashed := hex.EncodeToString(hash[:])
capsuleHashed := format + hashed
capsuleAnnotation := format + suffix
switch {
case len(capsuleAnnotation) <= maxAnnotationLength:
return capsuleAnnotation, nil
case len(capsuleHashed) <= maxAnnotationLength:
return capsuleHashed, nil
case len(hashed) <= maxAnnotationLength:
return hashed, nil
default:
return "", fmt.Errorf("the annotation name would exceed the maximum supported length (%d), skipping", maxAnnotationLength)
}
}
func UsedQuotaFor(resource fmt.Stringer) (string, error) {
return createAnnotation("quota.capsule.clastix.io/used-", resource)
}
func HardQuotaFor(resource fmt.Stringer) (string, error) {
return createAnnotation("quota.capsule.clastix.io/hard-", resource)
}

View File

@@ -1,289 +0,0 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package v1beta2
import (
"fmt"
"strconv"
"strings"
"sigs.k8s.io/controller-runtime/pkg/conversion"
capsulev1beta1 "github.com/projectcapsule/capsule/api/v1beta1"
"github.com/projectcapsule/capsule/pkg/api"
)
func (in *Tenant) ConvertFrom(raw conversion.Hub) error {
src, ok := raw.(*capsulev1beta1.Tenant)
if !ok {
return fmt.Errorf("expected *capsulev1beta1.Tenant, got %T", raw)
}
annotations := src.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
in.ObjectMeta = src.ObjectMeta
in.Spec.Owners = make(OwnerListSpec, 0, len(src.Spec.Owners))
for index, owner := range src.Spec.Owners {
proxySettings := make([]ProxySettings, 0, len(owner.ProxyOperations))
for _, proxyOp := range owner.ProxyOperations {
ops := make([]ProxyOperation, 0, len(proxyOp.Operations))
for _, op := range proxyOp.Operations {
ops = append(ops, ProxyOperation(op))
}
proxySettings = append(proxySettings, ProxySettings{
Kind: ProxyServiceKind(proxyOp.Kind),
Operations: ops,
})
}
in.Spec.Owners = append(in.Spec.Owners, OwnerSpec{
Kind: OwnerKind(owner.Kind),
Name: owner.Name,
ClusterRoles: owner.GetRoles(*src, index),
ProxyOperations: proxySettings,
})
}
if nsOpts := src.Spec.NamespaceOptions; nsOpts != nil {
in.Spec.NamespaceOptions = &NamespaceOptions{}
in.Spec.NamespaceOptions.Quota = src.Spec.NamespaceOptions.Quota
in.Spec.NamespaceOptions.AdditionalMetadata = nsOpts.AdditionalMetadata
if value, found := annotations[api.ForbiddenNamespaceLabelsAnnotation]; found {
in.Spec.NamespaceOptions.ForbiddenLabels.Exact = strings.Split(value, ",")
delete(annotations, api.ForbiddenNamespaceLabelsAnnotation)
}
if value, found := annotations[api.ForbiddenNamespaceLabelsRegexpAnnotation]; found {
in.Spec.NamespaceOptions.ForbiddenLabels.Regex = value
delete(annotations, api.ForbiddenNamespaceLabelsRegexpAnnotation)
}
if value, found := annotations[api.ForbiddenNamespaceAnnotationsAnnotation]; found {
in.Spec.NamespaceOptions.ForbiddenAnnotations.Exact = strings.Split(value, ",")
delete(annotations, api.ForbiddenNamespaceAnnotationsAnnotation)
}
if value, found := annotations[api.ForbiddenNamespaceAnnotationsRegexpAnnotation]; found {
in.Spec.NamespaceOptions.ForbiddenAnnotations.Regex = value
delete(annotations, api.ForbiddenNamespaceAnnotationsRegexpAnnotation)
}
}
in.Spec.ServiceOptions = src.Spec.ServiceOptions
if src.Spec.StorageClasses != nil {
in.Spec.StorageClasses = &api.DefaultAllowedListSpec{
SelectorAllowedListSpec: api.SelectorAllowedListSpec{
AllowedListSpec: *src.Spec.StorageClasses,
},
}
}
if scope := src.Spec.IngressOptions.HostnameCollisionScope; len(scope) > 0 {
in.Spec.IngressOptions.HostnameCollisionScope = scope
}
v, found := annotations[capsulev1beta1.DenyWildcard]
if found {
value, err := strconv.ParseBool(v)
if err == nil {
in.Spec.IngressOptions.AllowWildcardHostnames = !value
delete(annotations, capsulev1beta1.DenyWildcard)
}
}
if ingressClass := src.Spec.IngressOptions.AllowedClasses; ingressClass != nil {
in.Spec.IngressOptions.AllowedClasses = &api.DefaultAllowedListSpec{
SelectorAllowedListSpec: api.SelectorAllowedListSpec{
AllowedListSpec: *ingressClass,
},
}
}
if hostnames := src.Spec.IngressOptions.AllowedHostnames; hostnames != nil {
in.Spec.IngressOptions.AllowedHostnames = hostnames
}
in.Spec.ContainerRegistries = src.Spec.ContainerRegistries
in.Spec.NodeSelector = src.Spec.NodeSelector
in.Spec.NetworkPolicies = src.Spec.NetworkPolicies
in.Spec.LimitRanges = src.Spec.LimitRanges
in.Spec.ResourceQuota = src.Spec.ResourceQuota
in.Spec.AdditionalRoleBindings = src.Spec.AdditionalRoleBindings
in.Spec.ImagePullPolicies = src.Spec.ImagePullPolicies
if src.Spec.PriorityClasses != nil {
in.Spec.PriorityClasses = &api.DefaultAllowedListSpec{
SelectorAllowedListSpec: api.SelectorAllowedListSpec{
AllowedListSpec: *src.Spec.PriorityClasses,
},
}
}
if v, found := annotations["capsule.clastix.io/cordon"]; found {
value, err := strconv.ParseBool(v)
if err == nil {
delete(annotations, "capsule.clastix.io/cordon")
}
in.Spec.Cordoned = value
}
if _, found := annotations[api.ProtectedTenantAnnotation]; found {
in.Spec.PreventDeletion = true
delete(annotations, api.ProtectedTenantAnnotation)
}
in.SetAnnotations(annotations)
in.Status.Namespaces = src.Status.Namespaces
in.Status.Size = src.Status.Size
switch src.Status.State {
case capsulev1beta1.TenantStateActive:
in.Status.State = TenantStateActive
case capsulev1beta1.TenantStateCordoned:
in.Status.State = TenantStateCordoned
default:
in.Status.State = TenantStateActive
}
return nil
}
func (in *Tenant) ConvertTo(raw conversion.Hub) error {
dst, ok := raw.(*capsulev1beta1.Tenant)
if !ok {
return fmt.Errorf("expected *capsulev1beta1.Tenant, got %T", raw)
}
annotations := in.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
dst.ObjectMeta = in.ObjectMeta
dst.Spec.Owners = make(capsulev1beta1.OwnerListSpec, 0, len(in.Spec.Owners))
for index, owner := range in.Spec.Owners {
proxySettings := make([]capsulev1beta1.ProxySettings, 0, len(owner.ProxyOperations))
for _, proxyOp := range owner.ProxyOperations {
ops := make([]capsulev1beta1.ProxyOperation, 0, len(proxyOp.Operations))
for _, op := range proxyOp.Operations {
ops = append(ops, capsulev1beta1.ProxyOperation(op))
}
proxySettings = append(proxySettings, capsulev1beta1.ProxySettings{
Kind: capsulev1beta1.ProxyServiceKind(proxyOp.Kind),
Operations: ops,
})
}
dst.Spec.Owners = append(dst.Spec.Owners, capsulev1beta1.OwnerSpec{
Kind: capsulev1beta1.OwnerKind(owner.Kind),
Name: owner.Name,
ProxyOperations: proxySettings,
})
if clusterRoles := owner.ClusterRoles; len(clusterRoles) > 0 {
annotations[fmt.Sprintf("%s/%d", capsulev1beta1.ClusterRoleNamesAnnotation, index)] = strings.Join(owner.ClusterRoles, ",")
}
}
if nsOpts := in.Spec.NamespaceOptions; nsOpts != nil {
dst.Spec.NamespaceOptions = &capsulev1beta1.NamespaceOptions{}
dst.Spec.NamespaceOptions.Quota = nsOpts.Quota
dst.Spec.NamespaceOptions.AdditionalMetadata = nsOpts.AdditionalMetadata
if exact := nsOpts.ForbiddenAnnotations.Exact; len(exact) > 0 {
annotations[api.ForbiddenNamespaceAnnotationsAnnotation] = strings.Join(exact, ",")
}
if regex := nsOpts.ForbiddenAnnotations.Regex; len(regex) > 0 {
annotations[api.ForbiddenNamespaceAnnotationsRegexpAnnotation] = regex
}
if exact := nsOpts.ForbiddenLabels.Exact; len(exact) > 0 {
annotations[api.ForbiddenNamespaceLabelsAnnotation] = strings.Join(exact, ",")
}
if regex := nsOpts.ForbiddenLabels.Regex; len(regex) > 0 {
annotations[api.ForbiddenNamespaceLabelsRegexpAnnotation] = regex
}
}
dst.Spec.ServiceOptions = in.Spec.ServiceOptions
if in.Spec.StorageClasses != nil {
dst.Spec.StorageClasses = &in.Spec.StorageClasses.AllowedListSpec
}
dst.Spec.IngressOptions.HostnameCollisionScope = in.Spec.IngressOptions.HostnameCollisionScope
if allowed := in.Spec.IngressOptions.AllowedClasses; allowed != nil {
dst.Spec.IngressOptions.AllowedClasses = &allowed.AllowedListSpec
}
if allowed := in.Spec.IngressOptions.AllowedHostnames; allowed != nil {
dst.Spec.IngressOptions.AllowedHostnames = allowed
}
annotations[capsulev1beta1.DenyWildcard] = fmt.Sprintf("%t", !in.Spec.IngressOptions.AllowWildcardHostnames)
if allowed := in.Spec.ContainerRegistries; allowed != nil {
dst.Spec.ContainerRegistries = allowed
}
dst.Spec.NodeSelector = in.Spec.NodeSelector
dst.Spec.NetworkPolicies = in.Spec.NetworkPolicies
dst.Spec.LimitRanges = in.Spec.LimitRanges
dst.Spec.ResourceQuota = in.Spec.ResourceQuota
dst.Spec.AdditionalRoleBindings = in.Spec.AdditionalRoleBindings
dst.Spec.ImagePullPolicies = in.Spec.ImagePullPolicies
if in.Spec.PriorityClasses != nil {
dst.Spec.PriorityClasses = &in.Spec.PriorityClasses.AllowedListSpec
}
if in.Spec.PreventDeletion {
annotations[api.ProtectedTenantAnnotation] = "true" //nolint:goconst
}
if in.Spec.Cordoned {
annotations["capsule.clastix.io/cordon"] = "true"
}
dst.SetAnnotations(annotations)
dst.Status.Size = in.Status.Size
dst.Status.Namespaces = in.Status.Namespaces
switch in.Status.State {
case TenantStateActive:
dst.Status.State = capsulev1beta1.TenantStateActive
case TenantStateCordoned:
dst.Status.State = capsulev1beta1.TenantStateCordoned
default:
dst.Status.State = capsulev1beta1.TenantStateActive
}
return nil
}

View File

@@ -1,38 +0,0 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package v1beta2
import (
"sort"
corev1 "k8s.io/api/core/v1"
)
func (in *Tenant) IsFull() bool {
// we don't have limits on assigned Namespaces
if in.Spec.NamespaceOptions == nil || in.Spec.NamespaceOptions.Quota == nil {
return false
}
return len(in.Status.Namespaces) >= int(*in.Spec.NamespaceOptions.Quota)
}
func (in *Tenant) AssignNamespaces(namespaces []corev1.Namespace) {
var l []string
for _, ns := range namespaces {
if ns.Status.Phase == corev1.NamespaceActive {
l = append(l, ns.GetName())
}
}
sort.Strings(l)
in.Status.Namespaces = l
in.Status.Size = uint(len(l))
}
func (in *Tenant) GetOwnerProxySettings(name string, kind OwnerKind) []ProxySettings {
return in.Spec.Owners.FindOwner(name, kind).ProxyOperations
}

View File

@@ -1,23 +0,0 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package v1beta2
// +kubebuilder:validation:Enum=Cordoned;Active
type tenantState string
const (
TenantStateActive tenantState = "Active"
TenantStateCordoned tenantState = "Cordoned"
)
// Returns the observed state of the Tenant.
type TenantStatus struct {
// +kubebuilder:default=Active
// The operational state of the Tenant. Possible values are "Active", "Cordoned".
State tenantState `json:"state"`
// How many namespaces are assigned to the Tenant.
Size uint `json:"size"`
// List of namespaces assigned to the Tenant.
Namespaces []string `json:"namespaces,omitempty"`
}

View File

@@ -1,95 +0,0 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package v1beta2
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/projectcapsule/capsule/pkg/api"
)
// TenantSpec defines the desired state of Tenant.
type TenantSpec struct {
// Specifies the owners of the Tenant. Mandatory.
Owners OwnerListSpec `json:"owners"`
// Specifies options for the Namespaces, such as additional metadata or maximum number of namespaces allowed for that Tenant. Once the namespace quota assigned to the Tenant has been reached, the Tenant owner cannot create further namespaces. Optional.
NamespaceOptions *NamespaceOptions `json:"namespaceOptions,omitempty"`
// Specifies options for the Service, such as additional metadata or block of certain type of Services. Optional.
ServiceOptions *api.ServiceOptions `json:"serviceOptions,omitempty"`
// Specifies the allowed StorageClasses assigned to the Tenant.
// Capsule assures that all PersistentVolumeClaim resources created in the Tenant can use only one of the allowed StorageClasses.
// A default value can be specified, and all the PersistentVolumeClaim resources created will inherit the declared class.
// Optional.
StorageClasses *api.DefaultAllowedListSpec `json:"storageClasses,omitempty"`
// Specifies options for the Ingress resources, such as allowed hostnames and IngressClass. Optional.
IngressOptions IngressOptions `json:"ingressOptions,omitempty"`
// Specifies the trusted Image Registries assigned to the Tenant. Capsule assures that all Pods resources created in the Tenant can use only one of the allowed trusted registries. Optional.
ContainerRegistries *api.AllowedListSpec `json:"containerRegistries,omitempty"`
// Specifies the label to control the placement of pods on a given pool of worker nodes. All namespaces created within the Tenant will have the node selector annotation. This annotation tells the Kubernetes scheduler to place pods on the nodes having the selector label. Optional.
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
// Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
NetworkPolicies api.NetworkPolicySpec `json:"networkPolicies,omitempty"`
// Specifies the resource min/max usage restrictions to the Tenant. The assigned values are inherited by any namespace created in the Tenant. Optional.
LimitRanges api.LimitRangesSpec `json:"limitRanges,omitempty"`
// Specifies a list of ResourceQuota resources assigned to the Tenant. The assigned values are inherited by any namespace created in the Tenant. The Capsule operator aggregates ResourceQuota at Tenant level, so that the hard quota is never crossed for the given Tenant. This permits the Tenant owner to consume resources in the Tenant regardless of the namespace. Optional.
ResourceQuota api.ResourceQuotaSpec `json:"resourceQuotas,omitempty"`
// Specifies additional RoleBindings assigned to the Tenant. Capsule will ensure that all namespaces in the Tenant always contain the RoleBinding for the given ClusterRole. Optional.
AdditionalRoleBindings []api.AdditionalRoleBindingsSpec `json:"additionalRoleBindings,omitempty"`
// Specify the allowed values for the imagePullPolicies option in Pod resources. Capsule assures that all Pod resources created in the Tenant can use only one of the allowed policy. Optional.
ImagePullPolicies []api.ImagePullPolicySpec `json:"imagePullPolicies,omitempty"`
// Specifies the allowed RuntimeClasses assigned to the Tenant.
// Capsule assures that all Pods resources created in the Tenant can use only one of the allowed RuntimeClasses.
// Optional.
RuntimeClasses *api.SelectorAllowedListSpec `json:"runtimeClasses,omitempty"`
// Specifies the allowed priorityClasses assigned to the Tenant.
// Capsule assures that all Pods resources created in the Tenant can use only one of the allowed PriorityClasses.
// A default value can be specified, and all the Pod resources created will inherit the declared class.
// Optional.
PriorityClasses *api.DefaultAllowedListSpec `json:"priorityClasses,omitempty"`
// Toggling the Tenant resources cordoning, when enable resources cannot be deleted.
Cordoned bool `json:"cordoned,omitempty"`
// Prevent accidental deletion of the Tenant.
// When enabled, the deletion request will be declined.
PreventDeletion bool `json:"preventDeletion,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:storageversion
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster,shortName=tnt
// +kubebuilder:printcolumn:name="State",type="string",JSONPath=".status.state",description="The actual state of the Tenant"
// +kubebuilder:printcolumn:name="Namespace quota",type="integer",JSONPath=".spec.namespaceOptions.quota",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"
// +kubebuilder:printcolumn:name="Node selector",type="string",JSONPath=".spec.nodeSelector",description="Node Selector applied to Pods"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age"
// Tenant is the Schema for the tenants API.
type Tenant struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec TenantSpec `json:"spec,omitempty"`
Status TenantStatus `json:"status,omitempty"`
}
func (in *Tenant) GetNamespaces() (res []string) {
res = make([]string, 0, len(in.Status.Namespaces))
res = append(res, in.Status.Namespaces...)
return
}
// +kubebuilder:object:root=true
// TenantList contains a list of Tenant.
type TenantList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Tenant `json:"items"`
}
func init() {
SchemeBuilder.Register(&Tenant{}, &TenantList{})
}

View File

@@ -1,62 +0,0 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package v1beta2
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
)
// GlobalTenantResourceSpec defines the desired state of GlobalTenantResource.
type GlobalTenantResourceSpec struct {
// Defines the Tenant selector used target the tenants on which resources must be propagated.
TenantSelector metav1.LabelSelector `json:"tenantSelector,omitempty"`
TenantResourceSpec `json:",inline"`
}
// GlobalTenantResourceStatus defines the observed state of GlobalTenantResource.
type GlobalTenantResourceStatus struct {
// List of Tenants addressed by the GlobalTenantResource.
SelectedTenants []string `json:"selectedTenants"`
// List of the replicated resources for the given TenantResource.
ProcessedItems ProcessedItems `json:"processedItems"`
}
type ProcessedItems []ObjectReferenceStatus
func (p *ProcessedItems) AsSet() sets.Set[string] {
set := sets.New[string]()
for _, i := range *p {
set.Insert(i.String())
}
return set
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster
// GlobalTenantResource allows to propagate resource replications to a specific subset of Tenant resources.
type GlobalTenantResource struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec GlobalTenantResourceSpec `json:"spec,omitempty"`
Status GlobalTenantResourceStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// GlobalTenantResourceList contains a list of GlobalTenantResource.
type GlobalTenantResourceList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []GlobalTenantResource `json:"items"`
}
func init() {
SchemeBuilder.Register(&GlobalTenantResource{}, &GlobalTenantResourceList{})
}

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