Skip to content

Commit 5b88b01

Browse files
authored
Merge branch 'main' into feat/github-refresh-tokens
2 parents e11e24c + 920bc62 commit 5b88b01

File tree

8 files changed

+345
-0
lines changed

8 files changed

+345
-0
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package api
19+
20+
import (
21+
"github.com/apache/incubator-devlake/core/errors"
22+
coreModels "github.com/apache/incubator-devlake/core/models"
23+
"github.com/apache/incubator-devlake/core/plugin"
24+
helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
25+
"github.com/apache/incubator-devlake/helpers/srvhelper"
26+
"github.com/apache/incubator-devlake/plugins/q_dev/models"
27+
"github.com/apache/incubator-devlake/plugins/q_dev/tasks"
28+
)
29+
30+
func MakeDataSourcePipelinePlanV200(
31+
subtaskMetas []plugin.SubTaskMeta,
32+
connectionId uint64,
33+
bpScopes []*coreModels.BlueprintScope,
34+
) (coreModels.PipelinePlan, []plugin.Scope, errors.Error) {
35+
// load connection and scope from the db
36+
connection, err := dsHelper.ConnSrv.FindByPk(connectionId)
37+
if err != nil {
38+
return nil, nil, err
39+
}
40+
scopeDetails, err := dsHelper.ScopeSrv.MapScopeDetails(connectionId, bpScopes)
41+
if err != nil {
42+
return nil, nil, err
43+
}
44+
45+
plan, err := makeDataSourcePipelinePlanV200(subtaskMetas, scopeDetails, connection)
46+
if err != nil {
47+
return nil, nil, err
48+
}
49+
scopes, err := makeScopesV200(scopeDetails, connection)
50+
if err != nil {
51+
return nil, nil, err
52+
}
53+
54+
return plan, scopes, nil
55+
}
56+
57+
func makeDataSourcePipelinePlanV200(
58+
subtaskMetas []plugin.SubTaskMeta,
59+
scopeDetails []*srvhelper.ScopeDetail[models.QDevS3Slice, srvhelper.NoScopeConfig],
60+
connection *models.QDevConnection,
61+
) (coreModels.PipelinePlan, errors.Error) {
62+
plan := make(coreModels.PipelinePlan, len(scopeDetails))
63+
for i, scopeDetail := range scopeDetails {
64+
s3Slice := scopeDetail.Scope
65+
stage := plan[i]
66+
if stage == nil {
67+
stage = coreModels.PipelineStage{}
68+
}
69+
70+
// construct task options for q_dev
71+
op := &tasks.QDevOptions{
72+
ConnectionId: s3Slice.ConnectionId,
73+
S3Prefix: s3Slice.Prefix,
74+
}
75+
76+
// Pass empty entities array to enable all subtasks
77+
task, err := helper.MakePipelinePlanTask("q_dev", subtaskMetas, []string{}, op)
78+
if err != nil {
79+
return nil, err
80+
}
81+
stage = append(stage, task)
82+
plan[i] = stage
83+
}
84+
return plan, nil
85+
}
86+
87+
func makeScopesV200(
88+
scopeDetails []*srvhelper.ScopeDetail[models.QDevS3Slice, srvhelper.NoScopeConfig],
89+
connection *models.QDevConnection,
90+
) ([]plugin.Scope, errors.Error) {
91+
scopes := make([]plugin.Scope, 0)
92+
// For Q Developer metrics, we don't need to create domain layer scopes
93+
// The data is collected and stored directly in the tool layer
94+
return scopes, nil
95+
}

backend/plugins/q_dev/impl/impl.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/apache/incubator-devlake/core/context"
2424
"github.com/apache/incubator-devlake/core/dal"
2525
"github.com/apache/incubator-devlake/core/errors"
26+
coreModels "github.com/apache/incubator-devlake/core/models"
2627
"github.com/apache/incubator-devlake/core/plugin"
2728
helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
2829
"github.com/apache/incubator-devlake/plugins/q_dev/api"
@@ -39,6 +40,7 @@ var _ interface {
3940
plugin.PluginModel
4041
plugin.PluginSource
4142
plugin.PluginMigration
43+
plugin.DataSourcePluginBlueprintV200
4244
plugin.CloseablePluginTask
4345
} = (*QDev)(nil)
4446

@@ -170,3 +172,10 @@ func (p QDev) Close(taskCtx plugin.TaskContext) errors.Error {
170172
data.S3Client.Close()
171173
return nil
172174
}
175+
176+
func (p QDev) MakeDataSourcePipelinePlanV200(
177+
connectionId uint64,
178+
scopes []*coreModels.BlueprintScope,
179+
) (coreModels.PipelinePlan, []plugin.Scope, errors.Error) {
180+
return api.MakeDataSourcePipelinePlanV200(p.SubTaskMetas(), connectionId, scopes)
181+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package migrationscripts
19+
20+
import (
21+
"github.com/apache/incubator-devlake/core/context"
22+
"github.com/apache/incubator-devlake/core/errors"
23+
)
24+
25+
type addScopeConfigIdToS3Slice struct{}
26+
27+
func (*addScopeConfigIdToS3Slice) Up(basicRes context.BasicRes) errors.Error {
28+
db := basicRes.GetDal()
29+
30+
// Add scope_config_id column to _tool_q_dev_s3_slices table
31+
err := db.Exec(`
32+
ALTER TABLE _tool_q_dev_s3_slices
33+
ADD COLUMN scope_config_id BIGINT UNSIGNED DEFAULT 0
34+
`)
35+
if err != nil {
36+
return errors.Convert(err)
37+
}
38+
39+
return nil
40+
}
41+
42+
func (*addScopeConfigIdToS3Slice) Version() uint64 {
43+
return 20251123000001
44+
}
45+
46+
func (*addScopeConfigIdToS3Slice) Name() string {
47+
return "Add scope_config_id column to S3 slice table"
48+
}

backend/plugins/q_dev/models/migrationscripts/register.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@ func All() []plugin.MigrationScript {
2929
new(addDisplayNameFields),
3030
new(addMissingMetrics),
3131
new(addS3SliceTable),
32+
new(addScopeConfigIdToS3Slice),
3233
}
3334
}

backend/plugins/q_dev/tasks/identity_client.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ func NewQDevIdentityClient(connection *models.QDevConnection) (*QDevIdentityClie
7070
// ResolveUserDisplayName resolves a user ID to a human-readable display name
7171
// Returns the display name if found, otherwise returns the original userId as fallback
7272
func (client *QDevIdentityClient) ResolveUserDisplayName(userId string) (string, error) {
73+
// Check if client or IdentityStore is nil
74+
if client == nil || client.IdentityStore == nil {
75+
return userId, nil
76+
}
77+
7378
input := &identitystore.DescribeUserInput{
7479
IdentityStoreId: aws.String(client.StoreId),
7580
UserId: aws.String(userId),

backend/plugins/q_dev/tasks/s3_data_extractor.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,4 +318,5 @@ var ExtractQDevS3DataMeta = plugin.SubTaskMeta{
318318
EnabledByDefault: true,
319319
Description: "Extract data from S3 CSV files and save to database",
320320
DomainTypes: []string{plugin.DOMAIN_TYPE_CROSS},
321+
Dependencies: []*plugin.SubTaskMeta{&CollectQDevS3FilesMeta},
321322
}

backend/plugins/q_dev/tasks/s3_file_collector.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,5 @@ var CollectQDevS3FilesMeta = plugin.SubTaskMeta{
114114
EntryPoint: CollectQDevS3Files,
115115
EnabledByDefault: true,
116116
Description: "Collect S3 file metadata from AWS S3 bucket",
117+
DomainTypes: []string{plugin.DOMAIN_TYPE_CROSS},
117118
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
import { useState } from 'react';
20+
import { Button, Alert, Space } from 'antd';
21+
import { CheckCircleOutlined, ExclamationCircleOutlined, LoadingOutlined } from '@ant-design/icons';
22+
23+
import API from '@/api';
24+
import { operator } from '@/utils';
25+
26+
interface Props {
27+
plugin: string;
28+
connectionId?: ID;
29+
values: any;
30+
initialValues: any;
31+
disabled?: boolean;
32+
}
33+
34+
interface TestResult {
35+
success: boolean;
36+
message: string;
37+
details?: {
38+
s3Access?: boolean;
39+
identityCenterAccess?: boolean;
40+
};
41+
}
42+
43+
export const QDevConnectionTest = ({ plugin, connectionId, values, initialValues, disabled }: Props) => {
44+
const [testing, setTesting] = useState(false);
45+
const [testResult, setTestResult] = useState<TestResult | null>(null);
46+
47+
const handleTest = async () => {
48+
setTesting(true);
49+
setTestResult(null);
50+
51+
try {
52+
const [success, result] = await operator(
53+
() => {
54+
if (connectionId) {
55+
// Test existing connection with only changed values
56+
return API.connection.test(plugin, connectionId, {
57+
authType: values.authType !== initialValues.authType ? values.authType : undefined,
58+
accessKeyId: values.accessKeyId !== initialValues.accessKeyId ? values.accessKeyId : undefined,
59+
secretAccessKey: values.secretAccessKey !== initialValues.secretAccessKey ? values.secretAccessKey : undefined,
60+
region: values.region !== initialValues.region ? values.region : undefined,
61+
bucket: values.bucket !== initialValues.bucket ? values.bucket : undefined,
62+
identityStoreId: values.identityStoreId !== initialValues.identityStoreId ? values.identityStoreId : undefined,
63+
identityStoreRegion: values.identityStoreRegion !== initialValues.identityStoreRegion ? values.identityStoreRegion : undefined,
64+
rateLimitPerHour: values.rateLimitPerHour !== initialValues.rateLimitPerHour ? values.rateLimitPerHour : undefined,
65+
proxy: values.proxy !== initialValues.proxy ? values.proxy : undefined,
66+
} as any);
67+
} else {
68+
// Test new connection with all values
69+
return API.connection.testOld(plugin, {
70+
authType: values.authType || 'access_key',
71+
accessKeyId: values.accessKeyId || '',
72+
secretAccessKey: values.secretAccessKey || '',
73+
region: values.region || '',
74+
bucket: values.bucket || '',
75+
identityStoreId: values.identityStoreId || '',
76+
identityStoreRegion: values.identityStoreRegion || '',
77+
rateLimitPerHour: values.rateLimitPerHour || 20000,
78+
proxy: values.proxy || '',
79+
endpoint: '', // Not used by Q Developer
80+
token: '', // Not used by Q Developer
81+
} as any);
82+
}
83+
},
84+
{
85+
setOperating: () => {}, // We handle loading state ourselves
86+
hideToast: true, // We show our own success/error messages
87+
},
88+
);
89+
90+
if (success && result) {
91+
setTestResult({
92+
success: true,
93+
message: 'Connection test successful! AWS credentials and S3 access verified.',
94+
details: {
95+
s3Access: true,
96+
identityCenterAccess: values.identityStoreId ? true : undefined,
97+
},
98+
});
99+
} else {
100+
setTestResult({
101+
success: false,
102+
message: 'Connection test failed. Please check your configuration.',
103+
});
104+
}
105+
} catch (error: any) {
106+
let errorMessage = 'Connection test failed. Please check your configuration.';
107+
108+
if (error?.response?.data?.message) {
109+
errorMessage = error.response.data.message;
110+
} else if (error?.message) {
111+
errorMessage = error.message;
112+
}
113+
114+
// Provide more specific error messages based on common issues
115+
if (errorMessage.includes('InvalidAccessKeyId') || errorMessage.includes('SignatureDoesNotMatch')) {
116+
errorMessage = 'Invalid AWS credentials. Please check your Access Key ID and Secret Access Key.';
117+
} else if (errorMessage.includes('NoSuchBucket')) {
118+
errorMessage = 'S3 bucket not found. Please check the bucket name and region.';
119+
} else if (errorMessage.includes('AccessDenied')) {
120+
errorMessage = 'Access denied. Please check your AWS permissions for S3 and IAM Identity Center.';
121+
} else if (errorMessage.includes('InvalidBucketName')) {
122+
errorMessage = 'Invalid S3 bucket name. Please check the bucket name format.';
123+
} else if (errorMessage.includes('NoCredentialsError')) {
124+
errorMessage = 'AWS credentials not found. Please provide valid Access Key ID and Secret Access Key, or ensure IAM role is properly configured.';
125+
}
126+
127+
setTestResult({
128+
success: false,
129+
message: errorMessage,
130+
});
131+
} finally {
132+
setTesting(false);
133+
}
134+
};
135+
136+
const getAlertType = () => {
137+
if (!testResult) return undefined;
138+
return testResult.success ? 'success' : 'error';
139+
};
140+
141+
const getAlertIcon = () => {
142+
if (testing) return <LoadingOutlined />;
143+
if (!testResult) return undefined;
144+
return testResult.success ? <CheckCircleOutlined /> : <ExclamationCircleOutlined />;
145+
};
146+
147+
return (
148+
<Space direction="vertical" style={{ width: '100%' }}>
149+
<Button
150+
type="default"
151+
loading={testing}
152+
disabled={disabled || testing}
153+
onClick={handleTest}
154+
style={{ marginTop: 16 }}
155+
>
156+
{testing ? 'Testing Connection...' : 'Test Connection'}
157+
</Button>
158+
159+
{(testResult || testing) && (
160+
<Alert
161+
type={getAlertType()}
162+
icon={getAlertIcon()}
163+
message={testing ? 'Testing connection to AWS S3 and IAM Identity Center...' : testResult?.message}
164+
description={
165+
testResult?.success && testResult.details ? (
166+
<div>
167+
<div>✓ S3 Access: Verified</div>
168+
{testResult.details.identityCenterAccess && (
169+
<div>✓ IAM Identity Center: Configured</div>
170+
)}
171+
{!values.identityStoreId && (
172+
<div style={{ marginTop: 8, color: '#faad14' }}>
173+
⚠️ IAM Identity Center not configured - user display names will show as user IDs
174+
</div>
175+
)}
176+
</div>
177+
) : undefined
178+
}
179+
showIcon
180+
style={{ marginTop: 8 }}
181+
/>
182+
)}
183+
</Space>
184+
);
185+
};

0 commit comments

Comments
 (0)