diff --git a/backend/plugins/argocd/tasks/application_convertor.go b/backend/plugins/argocd/tasks/application_convertor.go new file mode 100644 index 00000000000..bc10f069f3d --- /dev/null +++ b/backend/plugins/argocd/tasks/application_convertor.go @@ -0,0 +1,121 @@ +/* +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. +*/ + +package tasks + +import ( + "fmt" + "reflect" + "strings" + + "github.com/apache/incubator-devlake/core/dal" + "github.com/apache/incubator-devlake/core/errors" + "github.com/apache/incubator-devlake/core/models/domainlayer" + "github.com/apache/incubator-devlake/core/models/domainlayer/devops" + "github.com/apache/incubator-devlake/core/models/domainlayer/didgen" + "github.com/apache/incubator-devlake/core/plugin" + "github.com/apache/incubator-devlake/helpers/pluginhelper/api" + "github.com/apache/incubator-devlake/plugins/argocd/models" +) + +var ConvertApplicationsMeta = plugin.SubTaskMeta{ + Name: "convertApplications", + EntryPoint: ConvertApplications, + EnabledByDefault: true, + Description: "Convert ArgoCD applications into CICD scopes", + DomainTypes: []string{plugin.DOMAIN_TYPE_CICD}, + DependencyTables: []string{models.ArgocdApplication{}.TableName()}, + ProductTables: []string{devops.CicdScope{}.TableName()}, +} + +func ConvertApplications(taskCtx plugin.SubTaskContext) errors.Error { + data := taskCtx.GetData().(*ArgocdTaskData) + db := taskCtx.GetDal() + + cursor, err := db.Cursor( + dal.From(&models.ArgocdApplication{}), + dal.Where("connection_id = ?", data.Options.ConnectionId), + ) + if err != nil { + return err + } + defer cursor.Close() + + scopeIdGen := didgen.NewDomainIdGenerator(&models.ArgocdApplication{}) + + converter, err := api.NewDataConverter(api.DataConverterArgs{ + InputRowType: reflect.TypeOf(models.ArgocdApplication{}), + Input: cursor, + RawDataSubTaskArgs: api.RawDataSubTaskArgs{ + Ctx: taskCtx, + Table: RAW_APPLICATION_TABLE, + Params: models.ArgocdApiParams{ + ConnectionId: data.Options.ConnectionId, + Name: data.Options.ApplicationName, + }, + }, + Convert: func(inputRow interface{}) ([]interface{}, errors.Error) { + application := inputRow.(*models.ArgocdApplication) + scopeId := scopeIdGen.Generate(application.ConnectionId, application.Name) + scope := buildCicdScopeFromApplication(application, scopeId) + return []interface{}{scope}, nil + }, + }) + if err != nil { + return err + } + + return converter.Execute() +} + +func buildCicdScopeFromApplication(app *models.ArgocdApplication, scopeId string) *devops.CicdScope { + scope := &devops.CicdScope{ + DomainEntity: domainlayer.NewDomainEntity(scopeId), + Name: app.Name, + Description: describeApplicationScope(app), + Url: firstNonEmpty(app.RepoURL, app.DestServer), + CreatedDate: app.CreatedDate, + } + return scope +} + +func describeApplicationScope(app *models.ArgocdApplication) string { + parts := make([]string, 0, 3) + if app.Project != "" { + parts = append(parts, fmt.Sprintf("Project: %s", app.Project)) + } + if app.Namespace != "" { + parts = append(parts, fmt.Sprintf("Namespace: %s", app.Namespace)) + } + if app.DestNamespace != "" || app.DestServer != "" { + dest := strings.TrimSpace(fmt.Sprintf("%s/%s", app.DestServer, app.DestNamespace)) + dest = strings.Trim(dest, "/") + if dest != "" { + parts = append(parts, fmt.Sprintf("Destination: %s", dest)) + } + } + return strings.Join(parts, " | ") +} + +func firstNonEmpty(values ...string) string { + for _, v := range values { + if strings.TrimSpace(v) != "" { + return v + } + } + return "" +} diff --git a/backend/plugins/argocd/tasks/application_convertor_test.go b/backend/plugins/argocd/tasks/application_convertor_test.go new file mode 100644 index 00000000000..f33c749f026 --- /dev/null +++ b/backend/plugins/argocd/tasks/application_convertor_test.go @@ -0,0 +1,52 @@ +/* +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. +*/ + +package tasks + +import ( + "testing" + "time" + + "github.com/apache/incubator-devlake/plugins/argocd/models" + "github.com/stretchr/testify/assert" +) + +func TestDescribeApplicationScopeBuildsSummary(t *testing.T) { + desc := describeApplicationScope(&models.ArgocdApplication{ + Project: "observability", + Namespace: "argocd", + DestServer: "https://k8s.example.com", + DestNamespace: "prod", + }) + assert.Equal(t, "Project: observability | Namespace: argocd | Destination: https://k8s.example.com/prod", desc) +} + +func TestBuildCicdScopeFromApplication(t *testing.T) { + created := time.Now() + scope := buildCicdScopeFromApplication(&models.ArgocdApplication{ + Name: "test-app", + RepoURL: "https://git.example.com/app.git", + CreatedDate: &created, + }, "argocd:app:1") + + assert.Equal(t, "argocd:app:1", scope.Id) + assert.Equal(t, "test-app", scope.Name) + assert.Equal(t, "https://git.example.com/app.git", scope.Url) + if assert.NotNil(t, scope.CreatedDate) { + assert.Equal(t, created.UTC(), scope.CreatedDate.UTC()) + } +} diff --git a/backend/plugins/argocd/tasks/register.go b/backend/plugins/argocd/tasks/register.go index 56478cd1ff1..2d556691226 100644 --- a/backend/plugins/argocd/tasks/register.go +++ b/backend/plugins/argocd/tasks/register.go @@ -30,6 +30,7 @@ func CollectDataTaskMetas() []plugin.SubTaskMeta { return []plugin.SubTaskMeta{ CollectApplicationsMeta, ExtractApplicationsMeta, + ConvertApplicationsMeta, CollectSyncOperationsMeta, ExtractSyncOperationsMeta, ConvertSyncOperationsMeta, diff --git a/config-ui/src/plugins/components/scope-config-form/index.tsx b/config-ui/src/plugins/components/scope-config-form/index.tsx index 5c27d84d454..0f45a5708ff 100644 --- a/config-ui/src/plugins/components/scope-config-form/index.tsx +++ b/config-ui/src/plugins/components/scope-config-form/index.tsx @@ -34,6 +34,7 @@ import { AzureTransformation } from '@/plugins/register/azure'; import { TapdTransformation } from '@/plugins/register/tapd'; import { BambooTransformation } from '@/plugins/register/bamboo'; import { CircleCITransformation } from '@/plugins/register/circleci'; +import { ArgoCDTransformation } from '@/plugins/register/argocd'; import { DOC_URL } from '@/release'; import { operator } from '@/utils'; @@ -86,7 +87,7 @@ export const ScopeConfigForm = ({ setName(forceCreate ? `${res.name}-copy` : res.name); setEntities(res.entities ?? []); setTransformation(omit(res, ['id', 'connectionId', 'name', 'entities', 'createdAt', 'updatedAt'])); - } catch {} + } catch { } })(); }, [scopeConfigId]); @@ -193,6 +194,14 @@ export const ScopeConfigForm = ({ )}
+ {plugin === 'argocd' && ( + + )} + {plugin === 'azuredevops' && (