Skip to content

Commit 17837c0

Browse files
authored
feat: support iam in q dev (#8595)
1 parent ae3edc9 commit 17837c0

File tree

4 files changed

+100
-41
lines changed

4 files changed

+100
-41
lines changed

config-ui/src/plugins/register/q-dev/config.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export const QDevConfig: IPluginConfig = {
3131
docLink: 'https://devlake.apache.org/docs/UserManual/plugins/qdev',
3232
initialValues: {
3333
name: '',
34+
authType: 'access_key',
3435
accessKeyId: '',
3536
secretAccessKey: '',
3637
region: 'us-east-1',

config-ui/src/plugins/register/q-dev/connection-fields/aws-credentials.tsx

Lines changed: 83 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
*/
1818

1919
import { ChangeEvent, useEffect, useMemo, useRef } from 'react';
20-
import { Input } from 'antd';
20+
import { Input, Radio } from 'antd';
2121

2222
import { Block } from '@/components';
2323

@@ -32,7 +32,12 @@ interface Props {
3232
const ACCESS_KEY_PATTERN = /^[A-Z0-9]{16,32}$/;
3333
const REGION_PATTERN = /^[a-z]{2}-[a-z]+-\d$/;
3434

35-
const syncError = (key: string, error: string, setErrors: (errors: any) => void, ref: React.MutableRefObject<string | undefined>) => {
35+
const syncError = (
36+
key: string,
37+
error: string,
38+
setErrors: (errors: any) => void,
39+
ref: React.MutableRefObject<string | undefined>,
40+
) => {
3641
if (ref.current !== error) {
3742
ref.current = error;
3843
setErrors({ [key]: error });
@@ -42,9 +47,18 @@ const syncError = (key: string, error: string, setErrors: (errors: any) => void,
4247
export const AwsCredentials = ({ type, initialValues, values, setValues, setErrors }: Props) => {
4348
const isUpdate = type === 'update';
4449

50+
const authType = values.authType ?? 'access_key';
4551
const accessKeyId = values.accessKeyId ?? '';
4652
const secretAccessKey = values.secretAccessKey ?? '';
4753
const region = values.region ?? '';
54+
55+
const isAccessKeyAuth = authType === 'access_key';
56+
57+
useEffect(() => {
58+
if (values.authType === undefined) {
59+
setValues({ authType: initialValues.authType ?? 'access_key' });
60+
}
61+
}, [initialValues.authType, values.authType, setValues]);
4862

4963
useEffect(() => {
5064
if (values.accessKeyId === undefined) {
@@ -65,31 +79,33 @@ export const AwsCredentials = ({ type, initialValues, values, setValues, setErro
6579
}, [initialValues.region, values.region, setValues]);
6680

6781
const accessKeyError = useMemo(() => {
82+
if (!isAccessKeyAuth) return ''; // Not required for IAM role auth
6883
if (!accessKeyId) {
69-
return isUpdate ? '' : 'AWS Access Key ID is required.';
84+
return isUpdate ? '' : 'AWS Access Key ID is required';
7085
}
7186
if (!ACCESS_KEY_PATTERN.test(accessKeyId)) {
72-
return 'AWS Access Key ID must contain 16-32 upper case letters or digits.';
87+
return 'AWS Access Key ID must contain 16-32 uppercase letters or digits';
7388
}
7489
return '';
75-
}, [accessKeyId, isUpdate]);
90+
}, [accessKeyId, isUpdate, isAccessKeyAuth]);
7691

7792
const secretKeyError = useMemo(() => {
93+
if (!isAccessKeyAuth) return ''; // Not required for IAM role auth
7894
if (!secretAccessKey) {
79-
return isUpdate ? '' : 'AWS Secret Access Key is required.';
95+
return isUpdate ? '' : 'AWS Secret Access Key is required';
8096
}
8197
if (secretAccessKey && secretAccessKey.length < 40) {
82-
return 'AWS Secret Access Key looks too short.';
98+
return 'AWS Secret Access Key looks too short';
8399
}
84100
return '';
85-
}, [secretAccessKey, isUpdate]);
101+
}, [secretAccessKey, isUpdate, isAccessKeyAuth]);
86102

87103
const regionError = useMemo(() => {
88104
if (!region) {
89-
return 'AWS Region is required.';
105+
return 'AWS Region is required';
90106
}
91107
if (!REGION_PATTERN.test(region)) {
92-
return 'AWS Region should look like us-east-1.';
108+
return 'AWS Region should look like us-east-1';
93109
}
94110
return '';
95111
}, [region]);
@@ -122,31 +138,67 @@ export const AwsCredentials = ({ type, initialValues, values, setValues, setErro
122138
setValues({ region: e.target.value.trim() });
123139
};
124140

141+
const handleAuthTypeChange = (e: any) => {
142+
const newAuthType = e.target.value;
143+
setValues({ authType: newAuthType });
144+
145+
// Clear access key fields when switching to IAM role
146+
if (newAuthType === 'iam_role') {
147+
setValues({
148+
authType: newAuthType,
149+
accessKeyId: '',
150+
secretAccessKey: ''
151+
});
152+
}
153+
};
154+
125155
return (
126156
<>
127-
<Block title="AWS Access Key ID" description="Use the Access Key ID of the IAM user that can access your S3 bucket." required>
128-
<Input
129-
style={{ width: 386 }}
130-
placeholder="AKIAIOSFODNN7EXAMPLE"
131-
value={accessKeyId}
132-
onChange={handleAccessKeyChange}
133-
status={accessKeyError ? 'error' : ''}
134-
/>
135-
{accessKeyError && <div style={{ marginTop: 4, color: '#f5222d' }}>{accessKeyError}</div>}
136-
</Block>
137-
138-
<Block title="AWS Secret Access Key" description="Use the Secret Access Key paired with the Access Key ID." required>
139-
<Input.Password
140-
style={{ width: 386 }}
141-
placeholder={isUpdate ? '********' : 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'}
142-
value={secretAccessKey}
143-
onChange={handleSecretKeyChange}
144-
status={secretKeyError ? 'error' : ''}
145-
/>
146-
{secretKeyError && <div style={{ marginTop: 4, color: '#f5222d' }}>{secretKeyError}</div>}
157+
<Block title="Authentication Type" description="Choose how to authenticate with AWS" required>
158+
<Radio.Group value={authType} onChange={handleAuthTypeChange}>
159+
<Radio value="access_key">Access Key & Secret</Radio>
160+
<Radio value="iam_role">IAM Role (for EC2/ECS/Lambda)</Radio>
161+
</Radio.Group>
147162
</Block>
148163

149-
<Block title="AWS Region" description="Region of the S3 bucket, e.g. us-east-1." required>
164+
{isAccessKeyAuth && (
165+
<>
166+
<Block title="AWS Access Key ID" description="Use the Access Key ID of the IAM user that can access your S3 bucket" required>
167+
<Input
168+
style={{ width: 386 }}
169+
placeholder="AKIAIOSFODNN7EXAMPLE"
170+
value={accessKeyId}
171+
onChange={handleAccessKeyChange}
172+
status={accessKeyError ? 'error' : ''}
173+
/>
174+
{accessKeyError && <div style={{ marginTop: 4, color: '#f5222d' }}>{accessKeyError}</div>}
175+
</Block>
176+
177+
<Block title="AWS Secret Access Key" description="Use the Secret Access Key paired with the Access Key ID" required>
178+
<Input.Password
179+
style={{ width: 386 }}
180+
placeholder={isUpdate ? '********' : 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'}
181+
value={secretAccessKey}
182+
onChange={handleSecretKeyChange}
183+
status={secretKeyError ? 'error' : ''}
184+
/>
185+
{secretKeyError && <div style={{ marginTop: 4, color: '#f5222d' }}>{secretKeyError}</div>}
186+
</Block>
187+
</>
188+
)}
189+
190+
{!isAccessKeyAuth && (
191+
<Block title="IAM Role Authentication" description="DevLake will use the IAM role attached to the EC2 instance, ECS task, or Lambda function">
192+
<div style={{ padding: '12px', backgroundColor: '#f6f8fa', borderRadius: '6px', color: '#586069' }}>
193+
<p style={{ margin: 0 }}>
194+
Make sure the IAM role has the necessary S3 permissions to access your bucket.
195+
No additional credentials are required when using IAM role authentication.
196+
</p>
197+
</div>
198+
</Block>
199+
)}
200+
201+
<Block title="AWS Region" description="Region of the S3 bucket, e.g. us-east-1" required>
150202
<Input
151203
style={{ width: 386 }}
152204
placeholder="us-east-1"

config-ui/src/plugins/register/q-dev/data-scope.tsx

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,12 @@ type FormValues = {
170170
months?: number[];
171171
};
172172

173-
export const QDevDataScope = ({ connectionId: _connectionId, disabledItems, selectedItems, onChangeSelectedItems }: Props) => {
173+
export const QDevDataScope = ({
174+
connectionId: _connectionId,
175+
disabledItems,
176+
selectedItems,
177+
onChangeSelectedItems,
178+
}: Props) => {
174179
const [form] = Form.useForm<FormValues>();
175180

176181
const disabledIds = useMemo(() => new Set(disabledItems?.map((it) => String(it.id)) ?? []), [disabledItems]);
@@ -234,7 +239,9 @@ export const QDevDataScope = ({ connectionId: _connectionId, disabledItems, sele
234239
return;
235240
}
236241

237-
const uniqueMonths = Array.from(new Set(months)).map((m) => Number(m)).filter((m) => !Number.isNaN(m));
242+
const uniqueMonths = Array.from(new Set(months))
243+
.map((m) => Number(m))
244+
.filter((m) => !Number.isNaN(m));
238245
uniqueMonths.sort((a, b) => a - b);
239246

240247
uniqueMonths.forEach((month) => {
@@ -289,7 +296,11 @@ export const QDevDataScope = ({ connectionId: _connectionId, disabledItems, sele
289296
key: 'basePath',
290297
render: (_: unknown, item) => {
291298
const meta = extractScopeMeta(item);
292-
return meta.basePath ? <Typography.Text>{meta.basePath}</Typography.Text> : <Typography.Text type="secondary">(bucket root)</Typography.Text>;
299+
return meta.basePath ? (
300+
<Typography.Text>{meta.basePath}</Typography.Text>
301+
) : (
302+
<Typography.Text type="secondary">(bucket root)</Typography.Text>
303+
);
293304
},
294305
},
295306
{
@@ -340,12 +351,7 @@ export const QDevDataScope = ({ connectionId: _connectionId, disabledItems, sele
340351
<Input placeholder="user-report/AWSLogs/.../us-east-1" />
341352
</Form.Item>
342353

343-
<Form.Item
344-
label="Year"
345-
name="year"
346-
rules={[{ required: true, message: 'Enter year' }]}
347-
style={{ width: 160 }}
348-
>
354+
<Form.Item label="Year" name="year" rules={[{ required: true, message: 'Enter year' }]} style={{ width: 160 }}>
349355
<InputNumber min={2000} max={2100} style={{ width: '100%' }} />
350356
</Form.Item>
351357

config-ui/src/plugins/register/q-dev/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@
1616
*
1717
*/
1818

19-
export * from './config';
19+
export * from './config';

0 commit comments

Comments
 (0)