diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 12cb0bb..0151cca 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -59,3 +59,39 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + + test-deploy: + runs-on: ubuntu-latest + needs: build + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Helm + uses: azure/setup-helm@v3 + with: + version: v3.12.0 + + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + check-latest: true + + - name: Set up chart-testing + uses: helm/chart-testing-action@v2.4.0 + + - name: Run chart-testing (lint) + run: | + ct lint --charts chart/kronic --target-branch ${{ github.event.repository.default_branch }} + + - name: Create kind cluster + uses: helm/kind-action@v1.8.0 + + - name: Run chart-testing (install) + run: | + ct install --charts chart/kronic \ + --helm-extra-args '--timeout 60s' \ + --helm-extra-set-args '--set=image.tag=${{ needs.build.outputs.releaseTag }}' \ + --target-branch ${{ github.event.repository.default_branch }} diff --git a/README.md b/README.md index 1144459..172b25f 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ # Kronic ![Build / Test](https://github.com/mshade/kronic/actions/workflows/build.yaml/badge.svg) +[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) The simple Kubernetes CronJob admin UI. @@ -38,9 +39,15 @@ Kronic aims to be a simple admin UI / dashboard / manager to view, suspend, trig ## Configuration -Kronic can limit itself to a list of namespaces. Specify as a comma separated list in the `KRONIC_ALLOW_NAMESPACES` environment variable. +Kronic can be limited to a list of namespaces. Specify as a comma separated list in the `KRONIC_ALLOW_NAMESPACES` environment variable. The helm chart exposes this option. +Kronic also supports a namespaced installation. The `KRONIC_NAMESPACE_ONLY` +environment variable will limit Kronic to interacting only with CronJobs, Jobs +and Pods in its own namespace. Enabling this setting in the helm chart values +(`env.KRONIC_NAMESPACE_ONLY="true"`) will prevent creation of ClusterRole and +ClusterRolebinding, creating only a namespaced Role and RoleBinding. + ## Deploying to K8S @@ -101,7 +108,7 @@ Kronic is a small Flask app built with: - [x] CI/CD pipeline and versioning - [x] Helm chart - [x] Allow/Deny lists for namespaces -- [ ] Support a namespaced install (no cluster-wide view) +- [x] Support a namespaced install (no cluster-wide view) - [ ] Built-in auth options - [ ] NetworkPolicy in helm chart - [ ] Timeline / Cron schedule interpreter or display diff --git a/app.py b/app.py index 815e0c5..e8cdd4c 100644 --- a/app.py +++ b/app.py @@ -33,9 +33,10 @@ def wrapper(namespace, *args, **kwargs): data = { "error": f"Request to {namespace} denied due to KRONIC_ALLOW_NAMESPACES setting", "namespace": namespace, - "allowed_namespaces": config.ALLOW_NAMESPACES, } - if request.headers.get("content-type", None) == "application/json": + if request.headers.get( + "content-type", None + ) == "application/json" or request.base_url.startswith("/api/"): return data, 403 else: return render_template("denied.html", data=data) @@ -59,6 +60,12 @@ def healthz(): @app.route("/") @app.route("/namespaces/") def index(): + if config.NAMESPACE_ONLY: + return redirect( + f"/namespaces/{config.KRONIC_NAMESPACE}", + code=302, + ) + cronjobs = get_cronjobs() namespaces = {} # Count cronjobs per namespace @@ -140,6 +147,11 @@ def view_cronjob(namespace, cronjob_name): @app.route("/api/") def api_index(): + if config.NAMESPACE_ONLY: + return redirect( + f"/api/namespaces/{config.KRONIC_NAMESPACE}", + code=302, + ) # Return all cronjobs jobs = get_cronjobs() return jobs diff --git a/chart/kronic/ci/custom-auth-values.yaml b/chart/kronic/ci/custom-auth-values.yaml index 753c8ff..bc7dbbc 100644 --- a/chart/kronic/ci/custom-auth-values.yaml +++ b/chart/kronic/ci/custom-auth-values.yaml @@ -3,3 +3,6 @@ ingress: auth: enabled: true secretName: custom-basic-auth + +env: + KRONIC_NAMESPACE_ONLY: "true" diff --git a/chart/kronic/templates/deployment.yaml b/chart/kronic/templates/deployment.yaml index 199ddbc..78fde75 100644 --- a/chart/kronic/templates/deployment.yaml +++ b/chart/kronic/templates/deployment.yaml @@ -32,6 +32,10 @@ spec: image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} env: + - name: KRONIC_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace {{- range $name, $item := .Values.env }} - name: {{ $name }} value: {{ $item | quote }} diff --git a/chart/kronic/templates/rbac.yaml b/chart/kronic/templates/rbac.yaml index 3a1760f..b8473df 100644 --- a/chart/kronic/templates/rbac.yaml +++ b/chart/kronic/templates/rbac.yaml @@ -1,4 +1,45 @@ -{{- if .Values.rbac.enabled }} +{{- if and .Values.rbac.enabled }} + {{- if .Values.env.KRONIC_NAMESPACE_ONLY }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + {{- include "kronic.labels" . | nindent 4 }} + name: {{ include "kronic.fullname" . }} +rules: + - apiGroups: + - "" + resources: + - pods + - events + - pods/log + verbs: + - get + - list + - watch + - apiGroups: + - batch + resources: + - jobs + - cronjobs + - cronjobs/status + verbs: + - "*" +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + {{- include "kronic.labels" . | nindent 4 }} + name: {{ include "kronic.fullname" . }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "kronic.fullname" . }} +subjects: + - kind: ServiceAccount + name: {{ include "kronic.serviceAccountName" . }} + {{- else }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: @@ -40,4 +81,5 @@ subjects: - kind: ServiceAccount name: {{ include "kronic.serviceAccountName" . }} namespace: {{ .Release.Namespace | quote }} + {{- end }} {{- end }} diff --git a/chart/kronic/values.yaml b/chart/kronic/values.yaml index d9a3687..146d89e 100644 --- a/chart/kronic/values.yaml +++ b/chart/kronic/values.yaml @@ -12,12 +12,14 @@ image: env: # -- Comma separated list of namespaces to allow access to, eg: "staging,qa,example" KRONIC_ALLOW_NAMESPACES: "" + # -- Limit Kronic to its own namespace. Set to "true" to enable. + KRONIC_NAMESPACE_ONLY: "" # Specify whether to create ClusterRole and ClusterRoleBinding # for kronic. If disabled, you will need to handle permissions # manually. rbac: - # -- Create ClusterRole and ClusterRoleBindings for default cluster-wide cronjob/job/pod permissions + # -- Create ClusterRole, ClusterRoleBindings, Role, RoleBindings for cronjob/job/pod permissions. enabled: true serviceAccount: diff --git a/config.py b/config.py index b48da0e..4443420 100644 --- a/config.py +++ b/config.py @@ -1,7 +1,27 @@ +import logging import os +import sys + +log = logging.getLogger("app.config") ## Configuration # Comma separated list of namespaces to allow access to ALLOW_NAMESPACES = os.environ.get("KRONIC_ALLOW_NAMESPACES", None) + # Boolean of whether this is a test environment, disables kubeconfig setup TEST = os.environ.get("KRONIC_TEST", False) + +# Limit to local namespace. Supercedes `ALLOW_NAMESPACES` +NAMESPACE_ONLY = os.environ.get("KRONIC_NAMESPACE_ONLY", False) + +# Set allowed namespaces to the installed namespace only +if NAMESPACE_ONLY: + try: + KRONIC_NAMESPACE = os.environ["KRONIC_NAMESPACE"] + except KeyError as e: + log.error( + "ERROR: KRONIC_NAMESPACE variable not set and a NAMESPACE_ONLY mode was specified." + ) + sys.exit(1) + + ALLOW_NAMESPACES = KRONIC_NAMESPACE diff --git a/kron.py b/kron.py index b6e4098..b808b0e 100644 --- a/kron.py +++ b/kron.py @@ -31,6 +31,7 @@ def namespace_filter(func): Args: func (function): The function to wrap. Must have `namespace` as an arg to itself """ + def wrapper(namespace: str = None, *args, **kwargs): if config.ALLOW_NAMESPACES and namespace: if namespace in config.ALLOW_NAMESPACES.split(","): diff --git a/templates/denied.html b/templates/denied.html index 4d531c4..1fbdd5a 100644 --- a/templates/denied.html +++ b/templates/denied.html @@ -1,6 +1,6 @@ {% extends 'base.html' %} {% block content %}

{% block title %}Access denied to {{data.namespace}}{% endblock %}

-

Namespaces allowed: {{data.allowed_namespaces}}

+

{{data.error}}

-{% endblock %} \ No newline at end of file +{% endblock %}