This is an automated email from the ASF dual-hosted git repository.

klesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/main by this push:
     new 17837c04b feat: support iam in q dev (#8595)
17837c04b is described below

commit 17837c04ba8f9872ac2d3424b1a84138052e6a12
Author: Warren Chen <[email protected]>
AuthorDate: Tue Sep 30 10:49:17 2025 +0800

    feat: support iam in q dev (#8595)
---
 config-ui/src/plugins/register/q-dev/config.tsx    |   1 +
 .../q-dev/connection-fields/aws-credentials.tsx    | 114 +++++++++++++++------
 .../src/plugins/register/q-dev/data-scope.tsx      |  24 +++--
 config-ui/src/plugins/register/q-dev/index.ts      |   2 +-
 4 files changed, 100 insertions(+), 41 deletions(-)

diff --git a/config-ui/src/plugins/register/q-dev/config.tsx 
b/config-ui/src/plugins/register/q-dev/config.tsx
index cc1e89511..836d6417d 100644
--- a/config-ui/src/plugins/register/q-dev/config.tsx
+++ b/config-ui/src/plugins/register/q-dev/config.tsx
@@ -31,6 +31,7 @@ export const QDevConfig: IPluginConfig = {
     docLink: 'https://devlake.apache.org/docs/UserManual/plugins/qdev',
     initialValues: {
       name: '',
+      authType: 'access_key',
       accessKeyId: '',
       secretAccessKey: '',
       region: 'us-east-1',
diff --git 
a/config-ui/src/plugins/register/q-dev/connection-fields/aws-credentials.tsx 
b/config-ui/src/plugins/register/q-dev/connection-fields/aws-credentials.tsx
index c5fd1de10..19953d445 100644
--- a/config-ui/src/plugins/register/q-dev/connection-fields/aws-credentials.tsx
+++ b/config-ui/src/plugins/register/q-dev/connection-fields/aws-credentials.tsx
@@ -17,7 +17,7 @@
  */
 
 import { ChangeEvent, useEffect, useMemo, useRef } from 'react';
-import { Input } from 'antd';
+import { Input, Radio } from 'antd';
 
 import { Block } from '@/components';
 
@@ -32,7 +32,12 @@ interface Props {
 const ACCESS_KEY_PATTERN = /^[A-Z0-9]{16,32}$/;
 const REGION_PATTERN = /^[a-z]{2}-[a-z]+-\d$/;
 
-const syncError = (key: string, error: string, setErrors: (errors: any) => 
void, ref: React.MutableRefObject<string | undefined>) => {
+const syncError = (
+  key: string,
+  error: string,
+  setErrors: (errors: any) => void,
+  ref: React.MutableRefObject<string | undefined>,
+) => {
   if (ref.current !== error) {
     ref.current = error;
     setErrors({ [key]: error });
@@ -42,9 +47,18 @@ const syncError = (key: string, error: string, setErrors: 
(errors: any) => void,
 export const AwsCredentials = ({ type, initialValues, values, setValues, 
setErrors }: Props) => {
   const isUpdate = type === 'update';
 
+  const authType = values.authType ?? 'access_key';
   const accessKeyId = values.accessKeyId ?? '';
   const secretAccessKey = values.secretAccessKey ?? '';
   const region = values.region ?? '';
+  
+  const isAccessKeyAuth = authType === 'access_key';
+
+  useEffect(() => {
+    if (values.authType === undefined) {
+      setValues({ authType: initialValues.authType ?? 'access_key' });
+    }
+  }, [initialValues.authType, values.authType, setValues]);
 
   useEffect(() => {
     if (values.accessKeyId === undefined) {
@@ -65,31 +79,33 @@ export const AwsCredentials = ({ type, initialValues, 
values, setValues, setErro
   }, [initialValues.region, values.region, setValues]);
 
   const accessKeyError = useMemo(() => {
+    if (!isAccessKeyAuth) return ''; // Not required for IAM role auth
     if (!accessKeyId) {
-      return isUpdate ? '' : 'AWS Access Key ID is required.';
+      return isUpdate ? '' : 'AWS Access Key ID is required';
     }
     if (!ACCESS_KEY_PATTERN.test(accessKeyId)) {
-      return 'AWS Access Key ID must contain 16-32 upper case letters or 
digits.';
+      return 'AWS Access Key ID must contain 16-32 uppercase letters or 
digits';
     }
     return '';
-  }, [accessKeyId, isUpdate]);
+  }, [accessKeyId, isUpdate, isAccessKeyAuth]);
 
   const secretKeyError = useMemo(() => {
+    if (!isAccessKeyAuth) return ''; // Not required for IAM role auth
     if (!secretAccessKey) {
-      return isUpdate ? '' : 'AWS Secret Access Key is required.';
+      return isUpdate ? '' : 'AWS Secret Access Key is required';
     }
     if (secretAccessKey && secretAccessKey.length < 40) {
-      return 'AWS Secret Access Key looks too short.';
+      return 'AWS Secret Access Key looks too short';
     }
     return '';
-  }, [secretAccessKey, isUpdate]);
+  }, [secretAccessKey, isUpdate, isAccessKeyAuth]);
 
   const regionError = useMemo(() => {
     if (!region) {
-      return 'AWS Region is required.';
+      return 'AWS Region is required';
     }
     if (!REGION_PATTERN.test(region)) {
-      return 'AWS Region should look like us-east-1.';
+      return 'AWS Region should look like us-east-1';
     }
     return '';
   }, [region]);
@@ -122,31 +138,67 @@ export const AwsCredentials = ({ type, initialValues, 
values, setValues, setErro
     setValues({ region: e.target.value.trim() });
   };
 
+  const handleAuthTypeChange = (e: any) => {
+    const newAuthType = e.target.value;
+    setValues({ authType: newAuthType });
+    
+    // Clear access key fields when switching to IAM role
+    if (newAuthType === 'iam_role') {
+      setValues({ 
+        authType: newAuthType,
+        accessKeyId: '',
+        secretAccessKey: ''
+      });
+    }
+  };
+
   return (
     <>
-      <Block title="AWS Access Key ID" description="Use the Access Key ID of 
the IAM user that can access your S3 bucket." required>
-        <Input
-          style={{ width: 386 }}
-          placeholder="AKIAIOSFODNN7EXAMPLE"
-          value={accessKeyId}
-          onChange={handleAccessKeyChange}
-          status={accessKeyError ? 'error' : ''}
-        />
-        {accessKeyError && <div style={{ marginTop: 4, color: '#f5222d' 
}}>{accessKeyError}</div>}
-      </Block>
-
-      <Block title="AWS Secret Access Key" description="Use the Secret Access 
Key paired with the Access Key ID." required>
-        <Input.Password
-          style={{ width: 386 }}
-          placeholder={isUpdate ? '********' : 
'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'}
-          value={secretAccessKey}
-          onChange={handleSecretKeyChange}
-          status={secretKeyError ? 'error' : ''}
-        />
-        {secretKeyError && <div style={{ marginTop: 4, color: '#f5222d' 
}}>{secretKeyError}</div>}
+      <Block title="Authentication Type" description="Choose how to 
authenticate with AWS" required>
+        <Radio.Group value={authType} onChange={handleAuthTypeChange}>
+          <Radio value="access_key">Access Key & Secret</Radio>
+          <Radio value="iam_role">IAM Role (for EC2/ECS/Lambda)</Radio>
+        </Radio.Group>
       </Block>
 
-      <Block title="AWS Region" description="Region of the S3 bucket, e.g. 
us-east-1." required>
+      {isAccessKeyAuth && (
+        <>
+          <Block title="AWS Access Key ID" description="Use the Access Key ID 
of the IAM user that can access your S3 bucket" required>
+            <Input
+              style={{ width: 386 }}
+              placeholder="AKIAIOSFODNN7EXAMPLE"
+              value={accessKeyId}
+              onChange={handleAccessKeyChange}
+              status={accessKeyError ? 'error' : ''}
+            />
+            {accessKeyError && <div style={{ marginTop: 4, color: '#f5222d' 
}}>{accessKeyError}</div>}
+          </Block>
+
+          <Block title="AWS Secret Access Key" description="Use the Secret 
Access Key paired with the Access Key ID" required>
+            <Input.Password
+              style={{ width: 386 }}
+              placeholder={isUpdate ? '********' : 
'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'}
+              value={secretAccessKey}
+              onChange={handleSecretKeyChange}
+              status={secretKeyError ? 'error' : ''}
+            />
+            {secretKeyError && <div style={{ marginTop: 4, color: '#f5222d' 
}}>{secretKeyError}</div>}
+          </Block>
+        </>
+      )}
+
+      {!isAccessKeyAuth && (
+        <Block title="IAM Role Authentication" description="DevLake will use 
the IAM role attached to the EC2 instance, ECS task, or Lambda function">
+          <div style={{ padding: '12px', backgroundColor: '#f6f8fa', 
borderRadius: '6px', color: '#586069' }}>
+            <p style={{ margin: 0 }}>
+              Make sure the IAM role has the necessary S3 permissions to 
access your bucket.
+              No additional credentials are required when using IAM role 
authentication.
+            </p>
+          </div>
+        </Block>
+      )}
+
+      <Block title="AWS Region" description="Region of the S3 bucket, e.g. 
us-east-1" required>
         <Input
           style={{ width: 386 }}
           placeholder="us-east-1"
diff --git a/config-ui/src/plugins/register/q-dev/data-scope.tsx 
b/config-ui/src/plugins/register/q-dev/data-scope.tsx
index 97d641201..f6aa5a9b0 100644
--- a/config-ui/src/plugins/register/q-dev/data-scope.tsx
+++ b/config-ui/src/plugins/register/q-dev/data-scope.tsx
@@ -170,7 +170,12 @@ type FormValues = {
   months?: number[];
 };
 
-export const QDevDataScope = ({ connectionId: _connectionId, disabledItems, 
selectedItems, onChangeSelectedItems }: Props) => {
+export const QDevDataScope = ({
+  connectionId: _connectionId,
+  disabledItems,
+  selectedItems,
+  onChangeSelectedItems,
+}: Props) => {
   const [form] = Form.useForm<FormValues>();
 
   const disabledIds = useMemo(() => new Set(disabledItems?.map((it) => 
String(it.id)) ?? []), [disabledItems]);
@@ -234,7 +239,9 @@ export const QDevDataScope = ({ connectionId: 
_connectionId, disabledItems, sele
         return;
       }
 
-      const uniqueMonths = Array.from(new Set(months)).map((m) => 
Number(m)).filter((m) => !Number.isNaN(m));
+      const uniqueMonths = Array.from(new Set(months))
+        .map((m) => Number(m))
+        .filter((m) => !Number.isNaN(m));
       uniqueMonths.sort((a, b) => a - b);
 
       uniqueMonths.forEach((month) => {
@@ -289,7 +296,11 @@ export const QDevDataScope = ({ connectionId: 
_connectionId, disabledItems, sele
       key: 'basePath',
       render: (_: unknown, item) => {
         const meta = extractScopeMeta(item);
-        return meta.basePath ? 
<Typography.Text>{meta.basePath}</Typography.Text> : <Typography.Text 
type="secondary">(bucket root)</Typography.Text>;
+        return meta.basePath ? (
+          <Typography.Text>{meta.basePath}</Typography.Text>
+        ) : (
+          <Typography.Text type="secondary">(bucket root)</Typography.Text>
+        );
       },
     },
     {
@@ -340,12 +351,7 @@ export const QDevDataScope = ({ connectionId: 
_connectionId, disabledItems, sele
           <Input placeholder="user-report/AWSLogs/.../us-east-1" />
         </Form.Item>
 
-        <Form.Item
-          label="Year"
-          name="year"
-          rules={[{ required: true, message: 'Enter year' }]}
-          style={{ width: 160 }}
-        >
+        <Form.Item label="Year" name="year" rules={[{ required: true, message: 
'Enter year' }]} style={{ width: 160 }}>
           <InputNumber min={2000} max={2100} style={{ width: '100%' }} />
         </Form.Item>
 
diff --git a/config-ui/src/plugins/register/q-dev/index.ts 
b/config-ui/src/plugins/register/q-dev/index.ts
index 257c4bc7d..de415db39 100644
--- a/config-ui/src/plugins/register/q-dev/index.ts
+++ b/config-ui/src/plugins/register/q-dev/index.ts
@@ -16,4 +16,4 @@
  *
  */
 
-export * from './config';
\ No newline at end of file
+export * from './config';

Reply via email to