Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

❇️ support namespaced installation #16

Merged
merged 10 commits into from
Sep 12, 2023
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 %}