def test_psp(self, kube_version, psp_docs): """Test that helm errors when pspEnabled=False, and renders a good PodSecurityPolicy template when pspEnabled=True.""" with pytest.raises(CalledProcessError): docs = render_chart( kube_version=kube_version, values={"global": { "pspEnabled": False }}, show_only=[psp_docs["template"]], ) docs = render_chart( kube_version=kube_version, values={"global": { "pspEnabled": True }}, show_only=[psp_docs["template"]], ) assert len(docs) == 1 doc = docs[0] assert doc["kind"] == "PodSecurityPolicy" assert doc["apiVersion"] == "policy/v1beta1" assert doc["metadata"]["name"] == psp_docs["name"] assert "spec" in doc
def test_fluentd_clusterrolebinding(kube_version): """Test that helm renders a good ClusterRoleBinding template for fluentd when rbacEnabled=True.""" docs = render_chart( kube_version=kube_version, values={"global": { "rbacEnabled": True }}, show_only=["charts/fluentd/templates/fluentd-clusterrolebinding.yaml"], ) assert len(docs) == 1 doc = docs[0] assert doc["kind"] == "ClusterRoleBinding" assert doc["apiVersion"] == "rbac.authorization.k8s.io/v1" assert doc["metadata"]["name"] == "release-name-fluentd" assert len(doc["roleRef"]) > 0 assert len(doc["subjects"]) > 0 docs = render_chart( kube_version=kube_version, values={"global": { "rbacEnabled": False }}, show_only=["charts/fluentd/templates/fluentd-clusterrolebinding.yaml"], ) assert len(docs) == 0
def test_clusterrole(self, kube_version, clusterrole_docs): """Test that helm errors when pspEnabled=False, and renders a good ClusterRole template when pspEnabled=True.""" with pytest.raises(CalledProcessError): docs = render_chart( kube_version=kube_version, values={"global": { "pspEnabled": False }}, show_only=[clusterrole_docs["template"]], ) docs = render_chart( kube_version=kube_version, values={"global": { "pspEnabled": True }}, show_only=[clusterrole_docs["template"]], ) assert len(docs) == 1 doc = docs[0] assert doc["kind"] == "ClusterRole" assert doc["apiVersion"] == "rbac.authorization.k8s.io/v1" assert doc["metadata"]["name"] == clusterrole_docs["name"] assert "rules" in doc assert all( item in doc["rules"][0] for item in ["apiGroups", "resources", "resourceNames", "verbs"])
def test_rolebinding(self, kube_version, rolebinding_docs): """Test that helm errors when pspEnabled=False, and renders a good RoleBinding template when pspEnabled=True.""" with pytest.raises(CalledProcessError): docs = render_chart( kube_version=kube_version, values={"global": { "pspEnabled": False }}, show_only=[rolebinding_docs["template"]], ) docs = render_chart( kube_version=kube_version, values={"global": { "pspEnabled": True }}, show_only=[rolebinding_docs["template"]], ) assert len(docs) == 1 doc = docs[0] assert doc["kind"] == "RoleBinding" assert doc["apiVersion"] == "rbac.authorization.k8s.io/v1" assert doc["metadata"]["name"] == rolebinding_docs["name"] assert len(doc["roleRef"]) >= 1 assert len(doc["subjects"]) >= 1
def test_astronomer_namespace_pools_namespaces(self, kube_version): """Test that Namespaces resources are rendered properly when using namespacePools feature""" # If namespace Pools creation enabled -> create the namespaces namespaces = ["my-namespace-1", "my-namespace-2"] docs = render_chart( kube_version=kube_version, values={ "global": { "features": { "namespacePools": { "enabled": True, "namespaces": {"create": True, "names": namespaces}, } } } }, show_only=["charts/astronomer/templates/namespaces.yaml"], ) assert len(docs) == 2 for i in range(0, 2): namespace = docs[i] assert namespace["metadata"]["name"] == namespaces[i] assert namespace["kind"] == "Namespace" # If namespace Pools disabled -> should not create the namespaces docs = render_chart( kube_version=kube_version, values={ "global": { "features": { "namespacePools": { "enabled": False, "namespaces": {"create": True, "names": namespaces}, } } } }, show_only=["charts/astronomer/templates/namespaces.yaml"], ) assert len(docs) == 0 # If namespace pools enabled but namespaces creation disabled -> should not create the namespaces docs = render_chart( kube_version=kube_version, values={ "global": { "features": { "namespacePools": { "enabled": True, "namespaces": {"create": False, "names": namespaces}, } } } }, show_only=["charts/astronomer/templates/namespaces.yaml"], ) assert len(docs) == 0
def test_deployment_should_render_extra_env(kube_version): """Test that helm renders extra environment variables to the grafana-deployment resource when provided.""" docs = render_chart( kube_version=kube_version, values={"global": { "ssl": { "enabled": True } }}, show_only=[DEPLOYMENT_FILE], ) assert len(docs) == 1 doc = docs[0] assert doc["kind"] == "Deployment" grafana_container = None for container in doc["spec"]["template"]["spec"]["containers"]: if container["name"] == "grafana": grafana_container = container break assert grafana_container is not None assert len(grafana_container["env"]) == 3 docs = render_chart( kube_version=kube_version, values={ "global": { "ssl": { "enabled": True } }, "grafana": { "extraEnvVars": [ { "name": "GF_SMTP_ENABLED", "value": "true" }, { "name": "GF_SMTP_HOST", "value": "smtp.astronomer.io" }, ] }, }, show_only=[DEPLOYMENT_FILE], ) assert len(docs) == 1 doc = docs[0] grafana_container = None for container in doc["spec"]["template"]["spec"]["containers"]: if container["name"] == "grafana": grafana_container = container break assert grafana_container is not None assert len(grafana_container["env"]) == 5
def test_privateca_psp_disabled_cacertaddtohost_enabled(self, kube_version): """Test that nothing is rendered when psp is disabled and privateCaCertsAddToHost is enabled""" with pytest.raises(CalledProcessError): render_chart( kube_version=kube_version, show_only=["templates/trust-private-ca-on-all-nodes/psp.yaml"], values={ "global": { "pspEnabled": False, "privateCaCertsAddToHost": { "enabled": True, }, } }, )
def test_houston_pdb_cronjobs(self): """Test that pdbs do not touch houston cronjobs or workers""" templates = [ "charts/astronomer/templates/houston/cronjobs/houston-expire-deployments-cronjob.yaml", "charts/astronomer/templates/houston/cronjobs/houston-check-updates-cronjob.yaml", "charts/astronomer/templates/houston/cronjobs/houston-cleanup-deployments-cronjob.yaml", "charts/astronomer/templates/houston/cronjobs/houston-check-updates-cronjob.yaml", ] for show_only in templates: labels = render_chart( show_only=[show_only], values={ "astronomer": { "houston": { "expireDeployments": { "enabled": True } } } }, )[0]["spec"]["jobTemplate"]["spec"]["template"]["metadata"][ "labels"] assert (labels["component"] != "houston" ), f"ERROR: tempplate '{show_only}' matched houston"
def test_houston_api_pdb_deployment(self): """Houston pdb should have the right matchLabels""" template = "charts/astronomer/templates/houston/api/houston-deployment.yaml" labels = render_chart( show_only=[template])[0]["spec"]["template"]["metadata"]["labels"] assert labels["tier"] == "astronomer" assert labels["component"] == "houston"
def test_registry_sts_use_keyfile(self, kube_version): """Test some things that should apply to all cases.""" docs = render_chart( kube_version=kube_version, show_only=self.show_only, values={ "global": { "baseDomain": "example.com" }, "astronomer": { "registry": { "gcs": { "useKeyfile": True, "enabled": True } } }, }, ) assert len(docs) == 1 doc = docs[0] assert doc["kind"] == "Deployment" assert doc["apiVersion"] == "apps/v1" assert doc["metadata"]["name"] == "release-name-registry" assert doc["spec"]["template"]["spec"]["volumes"][2][ "name"] == "gcs-keyfile" assert (doc["spec"]["template"]["spec"]["containers"][0] ["volumeMounts"][3]["name"] == "gcs-keyfile")
def test_astronomer_config_syncer_cronjob_namespace_pool_enabled( self, kube_version): """Test that when namespace pool is enabled, config-syncer's container is configured to use namespaces from the pool""" namespaces = ["my-namespace-1", "my-namespace-2"] doc = render_chart( kube_version=kube_version, values={ "global": { "rbacEnabled": True, "features": { "namespacePools": { "enabled": True, "namespaces": { "create": True, "names": namespaces }, }, }, } }, show_only=[ "charts/astronomer/templates/config-syncer/config-syncer-cronjob.yaml", ], )[0] container = doc["spec"]["jobTemplate"]["spec"]["template"]["spec"][ "containers"][0] assert "--target-namespaces" in container["args"] assert ",".join(namespaces) in container["args"]
def test_postgresql_statefulset_with_private_registry_enabled( self, kube_version): """Test postgresql with privateRegistry=True.""" repository = "private-repository.example.com" docs = render_chart( kube_version=kube_version, values={ "global": { "privateRegistry": { "enabled": True, "repository": repository, }, "postgresqlEnabled": True, }, }, show_only=[ "charts/postgresql/templates/statefulset.yaml", ], ) for doc in docs: c_by_name = get_containers_by_name(doc=doc, include_init_containers=True) for name, container in c_by_name.items(): assert container["image"].startswith( repository ), f"Container named '{name}' does not use registry '{repository}': {container}"
def test_elasticsearch_securitycontext_overrides(self, kube_version): """Test ElasticSearch master, data with securityContext custom values""" docs = render_chart( kube_version=kube_version, values={ "elasticsearch": { "securityContext": { "capabilities": { "add": ["IPC_LOCK"] }, "runAsNonRoot": True, "runAsUser": 1001, } } }, show_only=[ "charts/elasticsearch/templates/master/es-master-statefulset.yaml", "charts/elasticsearch/templates/data/es-data-statefulset.yaml", ], ) assert len(docs) == 2 for doc in docs: pod_data = doc["spec"]["template"]["spec"]["containers"][0] assert pod_data["securityContext"]["capabilities"]["add"] == [ "IPC_LOCK" ] assert pod_data["securityContext"]["runAsNonRoot"] is True assert pod_data["securityContext"]["runAsUser"] == 1001
def test_houston_api_deployment(self, kube_version): docs = render_chart( kube_version=kube_version, show_only=[ "charts/astronomer/templates/houston/api/houston-deployment.yaml" ], ) assert len(docs) == 1 doc = docs[0] assert doc["kind"] == "Deployment" assert "annotations" not in doc["metadata"] assert ( { "tier": "astronomer", "component": "houston", "release": "release-name", } == doc["spec"]["selector"]["matchLabels"] == doc["spec"]["template"]["metadata"]["labels"] ) c_by_name = get_containers_by_name(doc, include_init_containers=True) assert c_by_name["houston-bootstrapper"]["image"].startswith( "quay.io/astronomer/ap-db-bootstrapper:" ) assert c_by_name["houston"]["image"].startswith( "quay.io/astronomer/ap-houston-api:" ) assert c_by_name["wait-for-db"]["image"].startswith( "quay.io/astronomer/ap-houston-api:" )
def test_deployment_should_render(kube_version): """Test that the grafana-deployment renders without error.""" docs = render_chart( kube_version=kube_version, show_only=[DEPLOYMENT_FILE], ) assert len(docs) == 1
def test_externalelasticsearch_with_secret(self, kube_version): """Test External ElasticSearch with secret passed from config/values.yaml.""" docs = render_chart( kube_version=kube_version, values={"global": {"customLogging": {"enabled": True, "secret": secret}}}, show_only=[ "charts/external-es-proxy/templates/external-es-proxy-deployment.yaml", "charts/external-es-proxy/templates/external-es-proxy-env-configmap.yaml", "charts/external-es-proxy/templates/external-es-proxy-configmap.yaml", "charts/external-es-proxy/templates/external-es-proxy-service.yaml", ], ) assert len(docs) == 4 doc = docs[0] assert doc["kind"] == "Deployment" assert doc["apiVersion"] == "apps/v1" assert doc["metadata"]["name"] == "release-name-external-es-proxy" expected_env = [{"name": "ES_SECRET", "value": secret}] assert expected_env == doc["spec"]["template"]["spec"]["containers"][0]["env"] assert "Service" == jmespath.search("kind", docs[3]) assert "release-name-external-es-proxy" == jmespath.search( "metadata.name", docs[3] ) assert "ClusterIP" == jmespath.search("spec.type", docs[3]) assert { "name": "secure-http", "protocol": "TCP", "port": 9200, } in jmespath.search("spec.ports", docs[3]) assert {"name": "http", "protocol": "TCP", "port": 9201} in jmespath.search( "spec.ports", docs[3] )
def test_nats_statefulset_with_metrics_and_resources(self, kube_version): """Test that nats statefulset renders good metrics exporter.""" docs = render_chart( kube_version=kube_version, show_only=["charts/nats/templates/statefulset.yaml"], values={ "nats": { "exporter": { "enabled": True, "resources": { "requests": { "cpu": "234m" } }, }, "nats": { "resources": { "requests": { "cpu": "123m" } } }, }, }, ) assert len(docs) == 1 c_by_name = get_containers_by_name(docs[0]) assert len(c_by_name) == 2 assert c_by_name["nats"]["resources"]["requests"]["cpu"] == "123m" assert c_by_name["metrics"]["resources"]["requests"]["cpu"] == "234m"
def test_astronomer_commander_deployment_upgrade_timeout( self, kube_version): """Test that helm renders a good deployment template for astronomer/commander. when upgrade timeout is set""" docs = render_chart( kube_version=kube_version, values={"astronomer": { "commander": { "upgradeTimeout": 600 } }}, show_only=[ "charts/astronomer/templates/commander/commander-deployment.yaml" ], ) assert len(docs) == 1 doc = docs[0] assert doc["kind"] == "Deployment" assert doc["apiVersion"] == "apps/v1" assert doc["metadata"]["name"] == "release-name-commander" assert any( image_name.startswith("quay.io/astronomer/ap-commander:") for image_name in jmespath.search( "spec.template.spec.containers[*].image", doc)) assert len(doc["spec"]["template"]["spec"]["containers"]) == 1 env_vars = { x["name"]: x["value"] for x in doc["spec"]["template"]["spec"]["containers"][0]["env"] } assert env_vars["COMMANDER_UPGRADE_TIMEOUT"] == "600"
def test_postgresql_statefulset_with_volumePermissions_enabled( self, kube_version): """Test postgresql statefulset when volumePermissions init container is enabled.""" docs = render_chart( kube_version=kube_version, values={ "global": { "postgresqlEnabled": True }, "postgresql": { "volumePermissions": { "enabled": True }, "persistence": { "enabled": True }, }, }, show_only=["charts/postgresql/templates/statefulset.yaml"], ) assert len(docs) == 1 doc = docs[0] assert doc["kind"] == "StatefulSet" assert doc["apiVersion"] == "apps/v1" assert doc["metadata"]["name"] == "release-name-postgresql" assert "initContainers" in doc["spec"]["template"]["spec"]
def test_prometheus_alerts_configmap_with_addition_alerts(self, kube_version): """Validate the prometheus alerts configmap renders additional alerts.""" additional_alerts = { "airflow": '- alert: ExampleAirflowAlert\n expr: 100 * sum(increase(airflow_ti_failures[30m])) / (sum(increase(airflow_ti_failures[30m])) + sum(increase(airflow_ti_successes[30m]))) > 10\n for: 15m\n labels:\n tier: airflow\n annotations:\n summary: The Astronomer Helm release {{ .Release.Name }} is failing task instances {{ printf "%q" "{{ printf \\"%.2f\\" $value }}%" }} of the time over the past 30 minutes\n description: Task instances failing above threshold\n', "platform": '- alert: ExamplePlatformAlert\n expr: count(rate(airflow_scheduler_heartbeat{}[1m]) <= 0) > 2\n for: 5m\n labels:\n tier: platform\n severity: critical\n annotations:\n summary: {{ printf "%q" "{{ $value }} airflow schedulers are not heartbeating" }}\n description: "If more than 2 Airflow Schedulers are not heartbeating for more than 5 minutes, this alarm fires."\n', } docs = render_chart( kube_version=kube_version, show_only=self.show_only, name="foo-name", namespace="bar-ns", values={"prometheus": {"additionalAlerts": additional_alerts}}, ) config_yaml = docs[0]["data"]["alerts"] assert re.search( r'.*The Astronomer Helm release foo-name is failing task instances "{{ printf \\"%.2f\\" \$value }}\%" of the time over the past 30 minutes.*', config_yaml, ) assert re.search( r".*If more than 2 Airflow Schedulers are not heartbeating for more than 5 minutes, this alarm fires..*", config_yaml, )
def test_astronomer_commander_rbac_multinamespace_mode_enabled( self, kube_version): """Test that if Houston's Airflow chart sub-configuration has multiNamespaceMode enabled, the rendered commander role has permissions to manage Cluster-level RBAC resources""" doc = render_chart( kube_version=kube_version, values={ "astronomer": { "houston": { "config": { "deployments": { "helm": { "airflow": { "multiNamespaceMode": True } } } } } } }, show_only=[ "charts/astronomer/templates/commander/commander-role.yaml" ], )[0] cluster_resources = ["clusterrolebindings", "clusterroles"] # check that there are rules for cluterroles and clusterrolebindings generated_resources = [ resource for rule in doc["rules"] if "resources" in rule for resource in rule["resources"] ] for resource in cluster_resources: assert resource in generated_resources
def test_log4shell(kube_version): """ Ensure remediation settings are in place for log4j log4shell CVE-2021-44228 https://github.com/astronomer/issues/issues/3880 """ docs = render_chart( kube_version=kube_version, show_only=[ "charts/elasticsearch/templates/client/es-client-deployment.yaml", "charts/elasticsearch/templates/data/es-data-statefulset.yaml", "charts/elasticsearch/templates/master/es-master-statefulset.yaml", ], ) containers = [ c for doc in docs for c in doc["spec"]["template"]["spec"]["containers"] ] # Assert that all containers contain at least one ES_JAVA_OPTS env var assert all( any(env_var["name"] == "ES_JAVA_OPTS" for env_var in c["env"]) for c in containers) # Assert that all ES_JAVA_OPTS env vars in all containers have the string -Dlog4j2.formatMsgNoLookups=true assert all("-Dlog4j2.formatMsgNoLookups=true" in env_var["value"] for c in containers for env_var in c["env"] if env_var["name"] == "ES_JAVA_OPTS")
def test_prometheus_alerts_configmap(self, kube_version): """Validate the prometheus alerts configmap and its embedded data.""" docs = render_chart( kube_version=kube_version, show_only=self.show_only, ) assert len(docs) == 1 doc = docs[0] assert doc["kind"] == "ConfigMap" assert doc["apiVersion"] == "v1" assert doc["metadata"]["name"] == "release-name-prometheus-alerts" # Validate the contents of an embedded yaml doc groups = yaml.safe_load(doc["data"]["alerts"])["groups"] for group in groups: assert isinstance(group.get("name"), str) assert isinstance(group.get("rules"), list) for rule in group["rules"]: if "alert" in rule: self.process_alert(rule) else: self.process_record(rule)
def test_astro_ui_deployment(self, kube_version): docs = render_chart( kube_version=kube_version, values={ "astronomer": { "astroUI": { "resources": { "requests": { "cpu": "100m", "memory": "256Mi" }, "limits": { "cpu": "500m", "memory": "1024Mi" }, } } } }, show_only=[ "charts/astronomer/templates/astro-ui/astro-ui-deployment.yaml" ], ) assert "Deployment" == jmespath.search("kind", docs[0]) assert "release-name-astro-ui" == jmespath.search( "metadata.name", docs[0]) assert "astro-ui" == jmespath.search( "spec.template.spec.containers[0].name", docs[0]) assert "500m" == jmespath.search( "spec.template.spec.containers[0].resources.limits.cpu", docs[0])
def test_nginx_type_loadbalancer(self): """Deployment works with type LoadBalancer and some LB customizations.""" doc = render_chart( values={ "nginx": { "serviceType": "LoadBalancer", "loadBalancerIP": "5.5.5.5", "loadBalancerSourceRanges": [ "1.1.1.1/32", "2.2.2.2/32", "3.3.3.3/32", ], } }, show_only=["charts/nginx/templates/nginx-service.yaml"], )[0] assert doc["spec"]["type"] == "LoadBalancer" assert doc["spec"]["loadBalancerIP"] == "5.5.5.5" assert doc["spec"]["loadBalancerSourceRanges"] == [ "1.1.1.1/32", "2.2.2.2/32", "3.3.3.3/32", ]
def test_houston_configmapwith_loggingsidecar_enabled(): """Validate the houston configmap and its embedded data with loggingSidecar.""" terminationEndpoint = "http://localhost:8000/quitquitquit" docs = render_chart( values={ "astronomer": { "houston": { "loggingSidecar": { "enabled": True } } } }, show_only=[ "charts/astronomer/templates/houston/houston-configmap.yaml" ], ) common_test_cases(docs) doc = docs[0] prod_yaml = yaml.safe_load(doc["data"]["production.yaml"]) log_cmd = 'log_cmd = "1> >( tee -a /var/log/sidecar-log-consumer/out.log ) 2> >( tee -a /var/log/sidecar-log-consumer/err.log >&2 )"' assert ( log_cmd in prod_yaml["deployments"]["helm"]["airflow"]["airflowLocalSettings"]) assert ( terminationEndpoint in prod_yaml["deployments"]["helm"]["airflow"]["airflowLocalSettings"]) assert prod_yaml["deployments"]["loggingSidecar"] == { "enabled": True, "name": "sidecar-log-consumer", "terminationEndpoint": "http://localhost:8000/quitquitquit", }
def test_astronomer_config_syncer_cronjob_namespace_pool_disabled( self, kube_version): """Test that when namespacePools is disabled, config-syncer cronjob is configured not to target any namespace.""" namespaces = ["my-namespace-1", "my-namespace-2"] doc = render_chart( kube_version=kube_version, values={ "global": { "rbacEnabled": True, "features": { "namespacePools": { "enabled": False, }, }, } }, show_only=[ "charts/astronomer/templates/config-syncer/config-syncer-cronjob.yaml", ], )[0] container = doc["spec"]["jobTemplate"]["spec"]["template"]["spec"][ "containers"][0] assert "--target-namespaces" not in container["args"] assert ",".join(namespaces) not in container["args"]
def test_houston_configmap_with_config_syncer_enabled(): """Validate the houston configmap and its embedded data with configSyncer enabled.""" docs = render_chart( values={"astronomer": { "configSyncer": { "enabled": True } }}, show_only=[ "charts/astronomer/templates/houston/houston-configmap.yaml" ], ) common_test_cases(docs) doc = docs[0] prod = yaml.safe_load(doc["data"]["production.yaml"]) assert prod["deployments"]["helm"]["airflow"]["webserver"][ "extraVolumeMounts"] == [{ "name": "signing-certificate", "mountPath": "/etc/airflow/tls", "readOnly": True, }] assert prod["deployments"]["helm"]["airflow"]["webserver"][ "extraVolumes"] == [{ "name": "signing-certificate", "secret": { "secretName": "release-name-houston-jwt-signing-certificate" }, }]
def test_cronjob_runtime_updates_enabled(self, kube_version): docs = render_chart( kube_version=kube_version, values={ "astronomer": { "houston": { "updateRuntimeCheck": { "enabled": True } } } }, show_only=[ "charts/astronomer/templates/houston/cronjobs/houston-check-runtime-updates.yaml" ], ) assert len(docs) == 1 doc = docs[0] assert doc["kind"] == "CronJob" assert doc["spec"]["schedule"] == "43 0 * * *" assert doc["spec"]["jobTemplate"]["spec"]["template"]["spec"][ "containers"][0]["args"] == [ "yarn", "check-runtime-updates", "--url=https://updates.astronomer.io/astronomer-runtime", ]
def get_chart_containers(k8s_version, chart_values, ignore_kind_list=[]): docs = render_chart( kube_version=k8s_version, values=chart_values, ) specs = jmespath.search( "[?spec.template.spec.containers].{name: metadata.name, kind: kind, containers: spec.template.spec.containers[*]}", docs, ) container_configs = {} ignore_kind_list = [ ignore_kind.lower() for ignore_kind in ignore_kind_list ] for spec in specs: kind = spec["kind"] if kind.lower() not in ignore_kind_list: name = spec["name"] for container in spec["containers"]: key = k8s_version + "_" + name + "_" + container["name"] container["key"] = key container["kind"] = kind container_configs[key] = container return container_configs