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';