Skip to content

Commit

Permalink
❇️ support namespaced installation (#16)
Browse files Browse the repository at this point in the history
* ❇️ initial pass at namespaced installation

* 🚀 add chart testing to build step since features depend on build

* 🐛 fix arg setting

* 🐛 python string of csv

* 🐛 redirect to api base url

* 🐛 return json response for api denials

* ♻️ remove allowed_ns from returned error

* 🔧 catch an unset namespace in namespaced mode

* 📝 document the namespaced mode feature

* 📝 license badge
  • Loading branch information
mshade authored Sep 12, 2023
1 parent 5fdaa04 commit e40efda
Show file tree
Hide file tree
Showing 10 changed files with 135 additions and 8 deletions.
36 changes: 36 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected]

- name: Run chart-testing (lint)
run: |
ct lint --charts chart/kronic --target-branch ${{ github.event.repository.default_branch }}
- name: Create kind cluster
uses: helm/[email protected]

- 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 }}
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
16 changes: 14 additions & 2 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions chart/kronic/ci/custom-auth-values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ ingress:
auth:
enabled: true
secretName: custom-basic-auth

env:
KRONIC_NAMESPACE_ONLY: "true"
4 changes: 4 additions & 0 deletions chart/kronic/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
44 changes: 43 additions & 1 deletion chart/kronic/templates/rbac.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -40,4 +81,5 @@ subjects:
- kind: ServiceAccount
name: {{ include "kronic.serviceAccountName" . }}
namespace: {{ .Release.Namespace | quote }}
{{- end }}
{{- end }}
4 changes: 3 additions & 1 deletion chart/kronic/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
20 changes: 20 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions kron.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(","):
Expand Down
4 changes: 2 additions & 2 deletions templates/denied.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{% extends 'base.html' %}
{% block content %}
<h2>{% block title %}Access denied to {{data.namespace}}{% endblock %}</h2>
<p>Namespaces allowed: {{data.allowed_namespaces}}</p>
<p>{{data.error}}</p>

{% endblock %}
{% endblock %}

0 comments on commit e40efda

Please sign in to comment.