This is an automated email from the ASF dual-hosted git repository.
bbovenzi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 1074b8eed6 Add Rendered k8s pod spec tab to ti details view (#39141)
1074b8eed6 is described below
commit 1074b8eed680af9668f11c63cf28e72db5470fde
Author: Brent Bovenzi <[email protected]>
AuthorDate: Thu May 2 11:21:45 2024 -0400
Add Rendered k8s pod spec tab to ti details view (#39141)
* Add Rendered k8s pod spec tab to ti details view
* Render yaml instead of json
* Fix rebase mistake
---
airflow/www/package.json | 4 +-
airflow/www/static/js/api/index.ts | 2 +
airflow/www/static/js/api/useRenderedK8s.ts | 43 +++++++++++++++++
airflow/www/static/js/dag/details/index.tsx | 23 ++++++++-
.../www/static/js/dag/details/taskInstance/Nav.tsx | 6 ---
.../js/dag/details/taskInstance/RenderedK8s.tsx | 55 ++++++++++++++++++++++
airflow/www/templates/airflow/dag.html | 2 +-
airflow/www/views.py | 44 ++++++++++++++++-
airflow/www/yarn.lock | 31 ++++++++++--
9 files changed, 196 insertions(+), 14 deletions(-)
diff --git a/airflow/www/package.json b/airflow/www/package.json
index 22b6f882d3..ef24c53a78 100644
--- a/airflow/www/package.json
+++ b/airflow/www/package.json
@@ -51,6 +51,7 @@
"@testing-library/jest-dom": "^5.16.0",
"@testing-library/react": "^13.0.0",
"@types/color": "^3.0.3",
+ "@types/json-to-pretty-yaml": "^1.2.1",
"@types/react": "^18.0.12",
"@types/react-dom": "^18.0.5",
"@types/react-syntax-highlighter": "^15.5.6",
@@ -126,11 +127,12 @@
"framer-motion": "^6.0.0",
"jquery": ">=3.5.0",
"jshint": "^2.13.4",
+ "json-to-pretty-yaml": "^1.2.2",
"lodash": "^4.17.21",
"moment-timezone": "^0.5.43",
"react": "^18.0.0",
"react-dom": "^18.0.0",
- "react-icons": "^4.9.0",
+ "react-icons": "^5.1.0",
"react-json-view": "^1.21.3",
"react-markdown": "^8.0.4",
"react-query": "^3.39.1",
diff --git a/airflow/www/static/js/api/index.ts
b/airflow/www/static/js/api/index.ts
index 4ba363a75c..c9bc4ed4c8 100644
--- a/airflow/www/static/js/api/index.ts
+++ b/airflow/www/static/js/api/index.ts
@@ -53,6 +53,7 @@ import { useTaskXcomEntry, useTaskXcomCollection } from
"./useTaskXcom";
import useEventLogs from "./useEventLogs";
import useCalendarData from "./useCalendarData";
import useCreateDatasetEvent from "./useCreateDatasetEvent";
+import useRenderedK8s from "./useRenderedK8s";
axios.interceptors.request.use((config) => {
config.paramsSerializer = {
@@ -102,4 +103,5 @@ export {
useEventLogs,
useCalendarData,
useCreateDatasetEvent,
+ useRenderedK8s,
};
diff --git a/airflow/www/static/js/api/useRenderedK8s.ts
b/airflow/www/static/js/api/useRenderedK8s.ts
new file mode 100644
index 0000000000..1b1828e569
--- /dev/null
+++ b/airflow/www/static/js/api/useRenderedK8s.ts
@@ -0,0 +1,43 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import axios, { AxiosResponse } from "axios";
+import { useQuery } from "react-query";
+
+import { getMetaValue } from "src/utils";
+
+const url = getMetaValue("rendered_k8s_data_url");
+
+const useRenderedK8s = (
+ runId: string | null,
+ taskId: string | null,
+ mapIndex?: number
+) =>
+ useQuery(
+ ["rendered_k8s", runId, taskId, mapIndex],
+ async () =>
+ axios.get<AxiosResponse, any>(url, {
+ params: { run_id: runId, task_id: taskId, map_index: mapIndex },
+ }),
+ {
+ enabled: !!runId && !!taskId,
+ }
+ );
+
+export default useRenderedK8s;
diff --git a/airflow/www/static/js/dag/details/index.tsx
b/airflow/www/static/js/dag/details/index.tsx
index 954792f968..33d2b68ef4 100644
--- a/airflow/www/static/js/dag/details/index.tsx
+++ b/airflow/www/static/js/dag/details/index.tsx
@@ -45,7 +45,7 @@ import {
MdPlagiarism,
MdEvent,
} from "react-icons/md";
-import { BiBracket } from "react-icons/bi";
+import { BiBracket, BiLogoKubernetes } from "react-icons/bi";
import URLSearchParamsWrapper from "src/utils/URLSearchParamWrapper";
import Header from "./Header";
@@ -68,6 +68,7 @@ import TaskDetails from "./task";
import AuditLog from "./AuditLog";
import RunDuration from "./dag/RunDuration";
import Calendar from "./dag/Calendar";
+import RenderedK8s from "./taskInstance/RenderedK8s";
const dagId = getMetaValue("dag_id")!;
@@ -79,6 +80,8 @@ interface Props {
ganttScrollRef: React.RefObject<HTMLDivElement>;
}
+const isK8sExecutor = getMetaValue("k8s_or_k8scelery_executor") === "True";
+
const tabToIndex = (tab?: string) => {
switch (tab) {
case "graph":
@@ -96,6 +99,8 @@ const tabToIndex = (tab?: string) => {
case "xcom":
case "calendar":
return 6;
+ case "rendered_k8s":
+ return 7;
case "details":
default:
return 0;
@@ -135,6 +140,9 @@ const indexToTab = (
if (!runId && !taskId) return "calendar";
if (isTaskInstance) return "xcom";
return undefined;
+ case 7:
+ if (isTaskInstance && isK8sExecutor) return "rendered_k8s";
+ return undefined;
default:
return undefined;
}
@@ -360,6 +368,14 @@ const Details = ({
</Text>
</Tab>
)}
+ {isTaskInstance && isK8sExecutor && (
+ <Tab>
+ <BiLogoKubernetes size={16} />
+ <Text as="strong" ml={1}>
+ K8s Pod Spec
+ </Text>
+ </Tab>
+ )}
{/* Match the styling of a tab but its actually a button */}
{!!taskId && !!runId && (
<Button
@@ -484,6 +500,11 @@ const Details = ({
/>
</TabPanel>
)}
+ {isTaskInstance && isK8sExecutor && (
+ <TabPanel height="100%">
+ <RenderedK8s />
+ </TabPanel>
+ )}
</TabPanels>
</Tabs>
</Flex>
diff --git a/airflow/www/static/js/dag/details/taskInstance/Nav.tsx
b/airflow/www/static/js/dag/details/taskInstance/Nav.tsx
index 88560d5dac..e24088085a 100644
--- a/airflow/www/static/js/dag/details/taskInstance/Nav.tsx
+++ b/airflow/www/static/js/dag/details/taskInstance/Nav.tsx
@@ -26,9 +26,7 @@ import type { Task } from "src/types";
import URLSearchParamsWrapper from "src/utils/URLSearchParamWrapper";
const dagId = getMetaValue("dag_id");
-const isK8sExecutor = getMetaValue("k8s_or_k8scelery_executor") === "True";
const taskInstancesUrl = getMetaValue("task_instances_list_url");
-const renderedK8sUrl = getMetaValue("rendered_k8s_url");
const taskUrl = getMetaValue("task_url");
const gridUrl = getMetaValue("grid_url");
@@ -49,7 +47,6 @@ const Nav = forwardRef<HTMLDivElement, Props>(
map_index: mapIndex ?? -1,
});
const detailsLink = `${taskUrl}&${params}`;
- const k8sLink = `${renderedK8sUrl}&${params}`;
const listParams = new URLSearchParamsWrapper({
_flt_3_dag_id: dagId,
_flt_3_task_id: taskId,
@@ -77,9 +74,6 @@ const Nav = forwardRef<HTMLDivElement, Props>(
{(!isMapped || mapIndex !== undefined) && (
<>
<LinkButton href={detailsLink}>More Details</LinkButton>
- {isK8sExecutor && (
- <LinkButton href={k8sLink}>K8s Pod Spec</LinkButton>
- )}
{isSubDag && (
<LinkButton href={subDagLink}>Zoom into SubDag</LinkButton>
)}
diff --git a/airflow/www/static/js/dag/details/taskInstance/RenderedK8s.tsx
b/airflow/www/static/js/dag/details/taskInstance/RenderedK8s.tsx
new file mode 100644
index 0000000000..459d191edd
--- /dev/null
+++ b/airflow/www/static/js/dag/details/taskInstance/RenderedK8s.tsx
@@ -0,0 +1,55 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { useRef } from "react";
+import { Code } from "@chakra-ui/react";
+import YAML from "json-to-pretty-yaml";
+
+import { getMetaValue, useOffsetTop } from "src/utils";
+
+import useSelection from "src/dag/useSelection";
+import { useRenderedK8s } from "src/api";
+
+const isK8sExecutor = getMetaValue("k8s_or_k8scelery_executor") === "True";
+
+const RenderedK8s = () => {
+ const {
+ selected: { runId, taskId, mapIndex },
+ } = useSelection();
+
+ const { data: renderedK8s } = useRenderedK8s(runId, taskId, mapIndex);
+
+ const k8sRef = useRef<HTMLPreElement>(null);
+ const offsetTop = useOffsetTop(k8sRef);
+
+ if (!isK8sExecutor || !runId || !taskId) return null;
+
+ return (
+ <Code
+ mt={3}
+ ref={k8sRef}
+ maxHeight={`calc(100% - ${offsetTop}px)`}
+ overflowY="auto"
+ >
+ <pre>{YAML.stringify(renderedK8s)}</pre>
+ </Code>
+ );
+};
+
+export default RenderedK8s;
diff --git a/airflow/www/templates/airflow/dag.html
b/airflow/www/templates/airflow/dag.html
index 29f535b4b6..c91d4b4454 100644
--- a/airflow/www/templates/airflow/dag.html
+++ b/airflow/www/templates/airflow/dag.html
@@ -65,7 +65,7 @@
<meta name="graph_url" content="{{ url_for('Airflow.graph',
dag_id=dag.dag_id, root=root) }}">
<meta name="task_url" content="{{ url_for('Airflow.task', dag_id=dag.dag_id)
}}">
<meta name="log_url" content="{{ url_for('Airflow.log', dag_id=dag.dag_id)
}}">
- <meta name="rendered_k8s_url" content="{{ url_for('Airflow.rendered_k8s',
dag_id=dag.dag_id) }}">
+ <meta name="rendered_k8s_data_url" content="{{
url_for('Airflow.rendered_k8s_data', dag_id=dag.dag_id) }}">
<meta name="task_instances_list_url" content="{{
url_for('TaskInstanceModelView.list') }}">
<meta name="tag_index_url" content="{{ url_for('Airflow.index',
tags='_TAG_NAME_') }}">
<meta name="mapped_instances_api" content="{{
url_for('/api/v1.airflow_api_connexion_endpoints_task_instance_endpoint_get_mapped_task_instances',
dag_id=dag.dag_id, dag_run_id='_DAG_RUN_ID_', task_id='_TASK_ID_') }}">
diff --git a/airflow/www/views.py b/airflow/www/views.py
index 306926c163..b2f8c2788b 100644
--- a/airflow/www/views.py
+++ b/airflow/www/views.py
@@ -1493,7 +1493,7 @@ class Airflow(AirflowBaseView):
form = DateTimeForm(data={"execution_date": dttm})
root = request.args.get("root", "")
map_index = request.args.get("map_index", -1, type=int)
- logger.info("Retrieving rendered templates.")
+ logger.info("Retrieving rendered k8s.")
dag: DAG = get_airflow_app().dag_bag.get_dag(dag_id)
task = dag.get_task(task_id)
@@ -1538,6 +1538,48 @@ class Airflow(AirflowBaseView):
title=title,
)
+ @expose("/object/rendered-k8s")
+ @auth.has_access_dag("GET", DagAccessEntity.TASK_INSTANCE)
+ @provide_session
+ def rendered_k8s_data(self, *, session: Session = NEW_SESSION):
+ """Get rendered k8s yaml."""
+ if not settings.IS_K8S_OR_K8SCELERY_EXECUTOR:
+ return {"error": "Not a k8s or k8s_celery executor"}, 404
+ # This part is only used for k8s executor so providers.cncf.kubernetes
must be installed
+ # with the get_rendered_k8s_spec method
+ from airflow.providers.cncf.kubernetes.template_rendering import
get_rendered_k8s_spec
+
+ dag_id = request.args.get("dag_id")
+ task_id = request.args.get("task_id")
+ if task_id is None:
+ return {"error": "Task id not passed in the request"}, 404
+ run_id = request.args.get("run_id")
+ map_index = request.args.get("map_index", -1, type=int)
+ logger.info("Retrieving rendered k8s data.")
+
+ dag: DAG = get_airflow_app().dag_bag.get_dag(dag_id)
+ task = dag.get_task(task_id)
+ dag_run = dag.get_dagrun(run_id=run_id, session=session)
+ ti = dag_run.get_task_instance(task_id=task.task_id,
map_index=map_index, session=session)
+
+ if not ti:
+ return {"error": f"can't find task instance {task.task_id}"}, 404
+ pod_spec = None
+ if not isinstance(ti, TaskInstance):
+ return {"error": f"{task.task_id} is not a task instance"}, 500
+ try:
+ pod_spec = get_rendered_k8s_spec(ti, session=session)
+ except AirflowException as e:
+ if not e.__cause__:
+ return {"error": f"Error rendering Kubernetes POD Spec: {e}"},
500
+ else:
+ tmp = Markup("Error rendering Kubernetes POD Spec:
{0}<br><br>Original error: {0.__cause__}")
+ return {"error": tmp.format(e)}, 500
+ except Exception as e:
+ return {"error": f"Error rendering Kubernetes Pod Spec: {e}"}, 500
+
+ return pod_spec
+
@expose("/get_logs_with_metadata")
@auth.has_access_dag("GET", DagAccessEntity.TASK_INSTANCE)
@auth.has_access_dag("GET", DagAccessEntity.TASK_LOGS)
diff --git a/airflow/www/yarn.lock b/airflow/www/yarn.lock
index b4ec5af7a2..73056df0e6 100644
--- a/airflow/www/yarn.lock
+++ b/airflow/www/yarn.lock
@@ -3360,6 +3360,11 @@
resolved
"https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad"
integrity
sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==
+"@types/json-to-pretty-yaml@^1.2.1":
+ version "1.2.1"
+ resolved
"https://registry.yarnpkg.com/@types/json-to-pretty-yaml/-/json-to-pretty-yaml-1.2.1.tgz#bf193455477295d83c78f73c08d956f74321193e"
+ integrity
sha512-+uOlBkCPkny6CE2a5IAR0Q21/ZE+90MsK7EfDblDdutcey+rbMDrp3i93M6MTwbMHFB75aIFR5fVXVcnLCkAiw==
+
"@types/json5@^0.0.29":
version "0.0.29"
resolved
"https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
@@ -8006,6 +8011,14 @@ json-stringify-safe@^5.0.1:
resolved
"https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
integrity
sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==
+json-to-pretty-yaml@^1.2.2:
+ version "1.2.2"
+ resolved
"https://registry.yarnpkg.com/json-to-pretty-yaml/-/json-to-pretty-yaml-1.2.2.tgz#f4cd0bd0a5e8fe1df25aaf5ba118b099fd992d5b"
+ integrity
sha512-rvm6hunfCcqegwYaG5T4yKJWxc9FXFgBVrcTZ4XfSVRwa5HA/Xs+vB/Eo9treYYHCeNM0nrSUr82V/M31Urc7A==
+ dependencies:
+ remedial "^1.0.7"
+ remove-trailing-spaces "^1.0.6"
+
json5@^1.0.2:
version "1.0.2"
resolved
"https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
@@ -9875,10 +9888,10 @@ react-focus-lock@^2.9.1:
use-callback-ref "^1.3.0"
use-sidecar "^1.1.2"
-react-icons@^4.9.0:
- version "4.9.0"
- resolved
"https://registry.yarnpkg.com/react-icons/-/react-icons-4.9.0.tgz#ba44f436a053393adb1bdcafbc5c158b7b70d2a3"
- integrity
sha512-ijUnFr//ycebOqujtqtV9PFS7JjhWg0QU6ykURVHuL4cbofvRCf3f6GMn9+fBktEFQOIVZnuAYLZdiyadRQRFg==
+react-icons@^5.1.0:
+ version "5.1.0"
+ resolved
"https://registry.yarnpkg.com/react-icons/-/react-icons-5.1.0.tgz#9e7533cc256571a610c2a1ec8a7a143fb1222943"
+ integrity
sha512-D3zug1270S4hbSlIRJ0CUS97QE1yNNKDjzQe3HqY0aefp2CBn9VgzgES27sRR2gOvFK+0CNx/BW0ggOESp6fqQ==
react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1:
version "16.13.1"
@@ -10248,11 +10261,21 @@ remark-rehype@^10.0.0:
mdast-util-to-hast "^12.1.0"
unified "^10.0.0"
+remedial@^1.0.7:
+ version "1.0.8"
+ resolved
"https://registry.yarnpkg.com/remedial/-/remedial-1.0.8.tgz#a5e4fd52a0e4956adbaf62da63a5a46a78c578a0"
+ integrity
sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==
+
[email protected]:
version "0.4.2"
resolved
"https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5"
integrity sha1-CkPTqq4egNuRngeuJUsoXZ4ce7U=
+remove-trailing-spaces@^1.0.6:
+ version "1.0.8"
+ resolved
"https://registry.yarnpkg.com/remove-trailing-spaces/-/remove-trailing-spaces-1.0.8.tgz#4354d22f3236374702f58ee373168f6d6887ada7"
+ integrity
sha512-O3vsMYfWighyFbTd8hk8VaSj9UAGENxAtX+//ugIst2RMk5e03h6RoIS+0ylsFxY1gvmPuAY/PO4It+gPEeySA==
+
require-directory@^2.1.1:
version "2.1.1"
resolved
"https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"