Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions chart/docs/production-guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,41 @@ In the ``values.yaml`` below secret-related parameters should be adjusted like:
# The maximum number of server connections to the result backend database from PgBouncer
resultBackendPoolSize: 5

Creating the first user
-----------------------

The chart does not create an Airflow user for you.

Earlier versions ran a create-user job by default that provisioned an ``admin`` account
with the password ``admin``. Those credentials were the same on every installation, so
anything able to reach the API server -- by default any workload in the cluster, since
``networkPolicies.enabled`` is ``false`` -- could sign in with the Admin role.

``createUserJob`` is therefore disabled by default and the chart ships no default
username or password. Enabling the job without supplying both fails the render with a
message saying so, rather than creating an account with well-known credentials.

Create the user yourself after installing:

.. code-block:: bash

kubectl exec -it deploy/<RELEASE_NAME>-api-server -- \
airflow users create -r Admin -u <username> -e <email> -f <first> -l <last> -p <password>

Or enable the job with credentials of your own:

.. code-block:: yaml

createUserJob:
enabled: true
defaultUser:
username: <username>
password: <password>

Values files are frequently committed to source control, so where your deployment
tooling supports it, supply the password from a Kubernetes Secret rather than writing it
into ``values.yaml``.

API Secret Key
--------------

Expand Down
7 changes: 7 additions & 0 deletions chart/newsfragments/72209.significant.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
``createUserJob`` is now disabled by default and creates no default user.

Earlier versions ran the job on every install and provisioned a FAB ``admin`` account with the password ``admin``. Those credentials were identical on every installation, so anything able to reach the API server could sign in with the Admin role.

The chart now ships no default ``createUserJob.defaultUser.username`` or ``password``, and enabling the job without setting both fails the render rather than creating an account with well-known credentials.

If you relied on the default account, either create the user yourself with ``airflow users create`` after installing, or set ``createUserJob.enabled: true`` together with a username and password of your own. See :doc:`production-guide` for both.
7 changes: 5 additions & 2 deletions chart/templates/NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,12 @@ Flower Dashboard: kubectl port-forward svc/{{ include "airflow.fullname" .
{{- end }}

{{- if .Values.createUserJob.enabled }}
Default user (Airflow UI) Login credentials:
User created by the create-user job (Airflow UI):
username: {{ .Values.createUserJob.defaultUser.username }}
password: {{ .Values.createUserJob.defaultUser.password }}
{{- else }}
No Airflow UI user has been created. Create one with:
kubectl exec -it deploy/{{ include "airflow.fullname" . }}-api-server --namespace {{ .Release.Namespace }} -- \
airflow users create -r Admin -u <username> -e <email> -f <first> -l <last> -p <password>
{{- end }}

{{- if .Values.postgresql.enabled }}
Expand Down
2 changes: 1 addition & 1 deletion chart/templates/jobs/create-user-job-serviceaccount.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
#########################################
## Airflow Create User Job ServiceAccount
#########################################
{{- if .Values.createUserJob.serviceAccount.create }}
{{- if and .Values.createUserJob.enabled .Values.createUserJob.serviceAccount.create }}
apiVersion: v1
kind: ServiceAccount
automountServiceAccountToken: {{ .Values.createUserJob.serviceAccount.automountServiceAccountToken }}
Expand Down
3 changes: 3 additions & 0 deletions chart/templates/jobs/create-user-job.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
## Airflow Create User Job
##########################
{{- if .Values.createUserJob.enabled }}
{{- if or (not .Values.createUserJob.defaultUser.username) (not .Values.createUserJob.defaultUser.password) }}
{{- fail "createUserJob.enabled is true but createUserJob.defaultUser.username and/or createUserJob.defaultUser.password are not set. Set both to credentials of your own, or leave createUserJob.enabled false and create the user yourself with `airflow users create`." }}
{{- end }}
{{- $nodeSelector := or .Values.createUserJob.nodeSelector .Values.nodeSelector }}
{{- $affinity := or .Values.createUserJob.affinity .Values.affinity }}
{{- $tolerations := or .Values.createUserJob.tolerations .Values.tolerations }}
Expand Down
7 changes: 6 additions & 1 deletion chart/tests/helm_tests/airflow_aux/test_airflow_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ def test_global_affinity_tolerations_topology_spread_constraints_and_node_select
}
],
"nodeSelector": {"type": "user-node"},
"createUserJob": {"enabled": True, "defaultUser": {"username": "admin", "password": "admin"}},
},
show_only=[
"templates/cleanup/cleanup-cronjob.yaml",
Expand Down Expand Up @@ -489,7 +490,11 @@ def test_priority_class_name(self):
"cleanup": {"enabled": True, "priorityClassName": "low-priority-airflow-cleanup-pods"},
"databaseCleanup": {"enabled": True, "priorityClassName": "low-priority-database-cleanup"},
"migrateDatabaseJob": {"priorityClassName": "low-priority-run-airflow-migrations"},
"createUserJob": {"priorityClassName": "low-priority-create-user-job"},
"createUserJob": {
"enabled": True,
"defaultUser": {"username": "admin", "password": "admin"},
"priorityClassName": "low-priority-create-user-job",
},
},
show_only=[
"templates/flower/flower-deployment.yaml",
Expand Down
17 changes: 16 additions & 1 deletion chart/tests/helm_tests/airflow_aux/test_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ class TestServiceAccountAnnotations:
(
{
"createUserJob": {
"enabled": True,
"defaultUser": {"username": "admin", "password": "admin"},
"serviceAccount": {
"annotations": {
"example": "createuser",
Expand Down Expand Up @@ -326,6 +328,13 @@ def test_tpl_rendered_annotations_airflow_3(self, values_key, show_only):
**({"executor": "KubernetesExecutor"} if values_key == "cleanup" else {}),
values_key: {
"enabled": True,
# The create-user job refuses to run without credentials of the
# deployer's own, so supply them the way an install would.
**(
{"defaultUser": {"username": "admin", "password": "admin"}}
if values_key == "createUserJob"
else {}
),
"serviceAccount": {
"annotations": {
"iam.gke.io/gcp-service-account": "{{ .Release.Name }}-sa@project.iam",
Expand Down Expand Up @@ -646,7 +655,13 @@ class TestJobAnnotationsTemplating:
("values", "show_only"),
[
(
{"createUserJob": {"annotations": {"job-ann": "{{ .Release.Name }}"}}},
{
"createUserJob": {
"enabled": True,
"defaultUser": {"username": "admin", "password": "admin"},
"annotations": {"job-ann": "{{ .Release.Name }}"},
}
},
"templates/jobs/create-user-job.yaml",
),
(
Expand Down
7 changes: 3 additions & 4 deletions chart/tests/helm_tests/airflow_aux/test_basic_helm_chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@

OBJECTS_STD_NAMING = {
("ServiceAccount", "test-basic-airflow-api-server"),
("ServiceAccount", "test-basic-airflow-create-user-job"),
("ServiceAccount", "test-basic-airflow-dag-processor"),
("ServiceAccount", "test-basic-airflow-migrate-database-job"),
("ServiceAccount", "test-basic-airflow-redis"),
Expand Down Expand Up @@ -64,7 +63,6 @@
("StatefulSet", "test-basic-airflow-worker"),
("StatefulSet", "test-basic-airflow-triggerer"),
("StatefulSet", "test-basic-postgresql"),
("Job", "test-basic-airflow-create-user"),
("Job", "test-basic-airflow-run-airflow-migrations"),
}

Expand All @@ -88,7 +86,6 @@ def test_basic_deployments(self):
}
expected = {
("ServiceAccount", "test-basic-api-server"),
("ServiceAccount", "test-basic-create-user-job"),
("ServiceAccount", "test-basic-dag-processor"),
("ServiceAccount", "test-basic-migrate-database-job"),
("ServiceAccount", "test-basic-redis"),
Expand Down Expand Up @@ -124,7 +121,6 @@ def test_basic_deployments(self):
("StatefulSet", "test-basic-postgresql"),
("StatefulSet", "test-basic-redis"),
("StatefulSet", "test-basic-worker"),
("Job", "test-basic-create-user"),
("Job", "test-basic-run-airflow-migrations"),
}
assert list_of_kind_names_tuples == expected
Expand Down Expand Up @@ -265,6 +261,7 @@ def test_labels_are_valid(self, executor, flower_routing_values, flower_routing_
"priorityClasses": [
{"name": "class1", "value": 10000},
],
"createUserJob": {"enabled": True, "defaultUser": {"username": "admin", "password": "admin"}},
}
values.update(flower_routing_values)

Expand Down Expand Up @@ -395,6 +392,7 @@ def test_labels_are_valid_on_job_templates(self):
"databaseCleanup": {"enabled": True},
"flower": {"enabled": True},
"postgresql": {"enabled": False}, # We won't check the objects created by the postgres chart
"createUserJob": {"enabled": True, "defaultUser": {"username": "admin", "password": "admin"}},
},
)
dict_of_labels_in_job_templates = {
Expand Down Expand Up @@ -441,6 +439,7 @@ def test_annotations_on_airflow_pods_in_deployment(self):
values={
"airflowPodAnnotations": {"test-annotation/safe-to-evict": "true"},
"flower": {"enabled": True},
"createUserJob": {"enabled": True, "defaultUser": {"username": "admin", "password": "admin"}},
},
show_only=show_only,
)
Expand Down
29 changes: 28 additions & 1 deletion chart/tests/helm_tests/airflow_aux/test_create_user_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,34 @@

import jmespath
import pytest
from chart_utils.helm_template_generator import render_chart
from chart_utils.helm_template_generator import render_chart as _render_chart


def _deep_merge(base: dict, override: dict) -> dict:
"""Merge ``override`` into ``base``, recursing into nested dicts."""
merged = dict(base)
for key, value in override.items():
if isinstance(value, dict) and isinstance(merged.get(key), dict):
merged[key] = _deep_merge(merged[key], value)
else:
merged[key] = value
return merged


def render_chart(values=None, **kwargs):
"""Render with the create-user job switched on and credentials supplied.

The chart does not create a user by default and refuses to run the job without a
username and password, so tests covering the job have to opt in the same way a
deployment would. Values passed by a test win over these.
"""
opt_in = {
"createUserJob": {
"enabled": True,
"defaultUser": {"username": "admin", "password": "admin"},
}
}
return _render_chart(values=_deep_merge(opt_in, values or {}), **kwargs)


class TestCreateUserJob:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ class TestExtraEnvEnvFrom:
def setup_class(cls) -> None:
values_str = textwrap.dedent(
"""
createUserJob:
enabled: true
defaultUser:
username: admin
password: admin
databaseCleanup:
enabled: true
applyCustomEnv: true
Expand Down
33 changes: 28 additions & 5 deletions chart/tests/helm_tests/security/test_rbac.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,11 @@ def test_deployments_no_rbac_no_sa(self):
"triggerer": {"serviceAccount": {"create": False}},
"statsd": {"serviceAccount": {"create": False}},
"otelCollector": {"tracesEnabled": True, "serviceAccount": {"create": False}},
"createUserJob": {"serviceAccount": {"create": False}},
"createUserJob": {
"enabled": True,
"defaultUser": {"username": "admin", "password": "admin"},
"serviceAccount": {"create": False},
},
"migrateDatabaseJob": {"serviceAccount": {"create": False}},
"flower": {"enabled": True, "serviceAccount": {"create": False}},
},
Expand All @@ -171,6 +175,10 @@ def test_deployments_no_rbac_with_sa(self):
k8s_objects = render_chart(
"test-rbac",
values={
"createUserJob": {
"enabled": True,
"defaultUser": {"username": "admin", "password": "admin"},
},
"fullnameOverride": "test-rbac",
"executor": "CeleryExecutor,KubernetesExecutor",
"rbac": {"create": False},
Expand Down Expand Up @@ -223,7 +231,11 @@ def test_deployments_with_rbac_no_sa(self):
"create": False,
},
},
"createUserJob": {"serviceAccount": {"create": False}},
"createUserJob": {
"enabled": True,
"defaultUser": {"username": "admin", "password": "admin"},
"serviceAccount": {"create": False},
},
"migrateDatabaseJob": {"serviceAccount": {"create": False}},
},
)
Expand All @@ -237,6 +249,10 @@ def test_deployments_with_rbac_with_sa(self):
k8s_objects = render_chart(
"test-rbac",
values={
"createUserJob": {
"enabled": True,
"defaultUser": {"username": "admin", "password": "admin"},
},
"fullnameOverride": "test-rbac",
"executor": "CeleryExecutor,KubernetesExecutor",
"cleanup": {"enabled": True},
Expand Down Expand Up @@ -314,7 +330,11 @@ def test_service_account_custom_names(self):
"name": CUSTOM_PGBOUNCER_NAME,
},
},
"createUserJob": {"serviceAccount": {"name": CUSTOM_CREATE_USER_JOBS_NAME}},
"createUserJob": {
"enabled": True,
"defaultUser": {"username": "admin", "password": "admin"},
"serviceAccount": {"name": CUSTOM_CREATE_USER_JOBS_NAME},
},
"migrateDatabaseJob": {"serviceAccount": {"name": CUSTOM_MIGRATE_DATABASE_JOBS_NAME}},
},
)
Expand Down Expand Up @@ -364,7 +384,11 @@ def test_service_account_custom_names_in_objects(self):
"name": CUSTOM_PGBOUNCER_NAME,
},
},
"createUserJob": {"serviceAccount": {"name": CUSTOM_CREATE_USER_JOBS_NAME}},
"createUserJob": {
"enabled": True,
"defaultUser": {"username": "admin", "password": "admin"},
"serviceAccount": {"name": CUSTOM_CREATE_USER_JOBS_NAME},
},
"migrateDatabaseJob": {"serviceAccount": {"name": CUSTOM_MIGRATE_DATABASE_JOBS_NAME}},
},
)
Expand Down Expand Up @@ -409,6 +433,5 @@ def test_service_account_without_resource(self):
"test-rbac-api-server",
"test-rbac-triggerer",
"test-rbac-migrate-database-job",
"test-rbac-create-user-job",
]
assert sorted(list_of_sa_names) == sorted(service_account_names)
2 changes: 1 addition & 1 deletion chart/tests/helm_tests/security/test_scc_rolebinding.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ def test_only_enable_components_role_binding_should_exists(self, executor):
"redis": {"enabled": True},
"triggerer": {"enabled": True},
"migrateDatabaseJob": {"enabled": True},
"createUserJob": {"enabled": True},
"createUserJob": {"enabled": True, "defaultUser": {"username": "admin", "password": "admin"}},
"cleanup": {"enabled": True},
"databaseCleanup": {"enabled": True},
"dagProcessor": {"enabled": True},
Expand Down
8 changes: 7 additions & 1 deletion chart/tests/helm_tests/security/test_security_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,13 @@ def test_disable_defaults_explicit_override_still_applied(self):
"spec.template.spec.securityContext",
),
(
{"createUserJob": {"securityContexts": {"pod": {"runAsUser": 8000}}}},
{
"createUserJob": {
"enabled": True,
"defaultUser": {"username": "admin", "password": "admin"},
"securityContexts": {"pod": {"runAsUser": 8000}},
}
},
"templates/jobs/create-user-job.yaml",
"spec.template.spec.securityContext",
),
Expand Down
10 changes: 5 additions & 5 deletions chart/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -5061,7 +5061,7 @@
"enabled": {
"description": "Whether the create user job should be created.",
"type": "boolean",
"default": true
"default": false
},
"defaultUser": {
"description": "Optional default Airflow user information",
Expand All @@ -5080,9 +5080,9 @@
"default": "Admin"
},
"username": {
"description": "Default user username.",
"description": "Default user username. Must be set when the job is enabled.",
"type": "string",
"default": "admin"
"default": ""
},
"email": {
"description": "Default user email address.",
Expand All @@ -5100,9 +5100,9 @@
"default": "user"
},
"password": {
"description": "Default user password.",
"description": "Default user password. Must be set when the job is enabled.",
"type": "string",
"default": "admin"
"default": ""
}
}
},
Expand Down
Loading
Loading