Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.

FROM mcr.microsoft.com/devcontainers/go:1-1.22-bookworm
FROM mcr.microsoft.com/devcontainers/go:1-1.23-bookworm

ENV PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/lib:/usr/local/lib/pkgconfig
ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib
ENV DEVLAKE_PLUGINS=bamboo,bitbucket,circleci,customize,dora,gitextractor,github,github_graphql,gitlab,jenkins,jira,org,pagerduty,refdiff,slack,sonarqube,trello,webhook
ENV DEVLAKE_PLUGINS=argocd,bamboo,bitbucket,circleci,customize,dora,gitextractor,github,github_graphql,gitlab,issue_trace,jenkins,jira,org,pagerduty,refdiff,slack,sonarqube,trello,webhook

RUN apt-get update -y
RUN apt-get install pkg-config python3-dev default-libmysqlclient-dev build-essential libpq-dev cmake -y
Expand Down
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,4 @@
}
},
"remoteUser": "root"
}
}
17 changes: 17 additions & 0 deletions backend/plugins/argocd/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!--
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.
-->
Please see details in the [Apache DevLake website](https://devlake.apache.org/docs/Plugins/argocd)
141 changes: 141 additions & 0 deletions backend/plugins/argocd/api/blueprint_v200.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
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 api

import (
"github.com/apache/incubator-devlake/core/errors"
coreModels "github.com/apache/incubator-devlake/core/models"
"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/core/utils"
"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
"github.com/apache/incubator-devlake/helpers/srvhelper"
"github.com/apache/incubator-devlake/plugins/argocd/models"
"github.com/apache/incubator-devlake/plugins/argocd/tasks"
)

func MakeDataSourcePipelinePlanV200(
subtaskMetas []plugin.SubTaskMeta,
connectionId uint64,
bpScopes []*coreModels.BlueprintScope,
) (coreModels.PipelinePlan, []plugin.Scope, errors.Error) {
connection, err := dsHelper.ConnSrv.FindByPk(connectionId)
if err != nil {
return nil, nil, err
}
scopeDetails, err := dsHelper.ScopeApi.MapScopeDetails(connectionId, bpScopes)
if err != nil {
// attempt auto-create missing scopes for blueprint (only name known)
cfg, _ := CreateDefaultScopeConfig(connectionId)
for _, bs := range bpScopes {
if _, findErr := dsHelper.ScopeSrv.ModelSrvHelper.FindByPk(connectionId, bs.ScopeId); findErr != nil {
app := &models.ArgocdApplication{}
app.ConnectionId = connectionId
app.Name = bs.ScopeId
if cfg != nil {
app.ScopeConfigId = cfg.ID
}
_ = dsHelper.ScopeSrv.ModelSrvHelper.CreateOrUpdate(app)
}
}
scopeDetails, err = dsHelper.ScopeApi.MapScopeDetails(connectionId, bpScopes)
if err != nil {
return nil, nil, err
}
}

plan, err := makeDataSourcePipelinePlanV200(subtaskMetas, scopeDetails, connection)
if err != nil {
return nil, nil, err
}
scopes, err := makeScopesV200(scopeDetails, connection)
if err != nil {
return nil, nil, err
}

return plan, scopes, nil
}

func makeDataSourcePipelinePlanV200(
subtaskMetas []plugin.SubTaskMeta,
scopeDetails []*srvhelper.ScopeDetail[models.ArgocdApplication, models.ArgocdScopeConfig],
connection *models.ArgocdConnection,
) (coreModels.PipelinePlan, errors.Error) {
plan := make(coreModels.PipelinePlan, len(scopeDetails))
for i, scopeDetail := range scopeDetails {
application := scopeDetail.Scope
scopeConfig := scopeDetail.ScopeConfig
if scopeConfig == nil {
scopeConfig = &models.ArgocdScopeConfig{}
}
stage := plan[i]
if stage == nil {
stage = coreModels.PipelineStage{}
}
scopeConfigId := application.ScopeConfigId
if scopeConfig != nil && scopeConfig.ID != 0 {
scopeConfigId = scopeConfig.ID
}
task, err := api.MakePipelinePlanTask(
"argocd",
subtaskMetas,
scopeConfig.Entities,
tasks.ArgocdOptions{
ConnectionId: connection.ID,
ApplicationName: application.Name,
ScopeConfigId: scopeConfigId,
},
)
if err != nil {
return nil, err
}
stage = append(stage, task)
plan[i] = stage
}

return plan, nil
}

func makeScopesV200(
scopeDetails []*srvhelper.ScopeDetail[models.ArgocdApplication, models.ArgocdScopeConfig],
connection *models.ArgocdConnection,
) ([]plugin.Scope, errors.Error) {
scopes := make([]plugin.Scope, 0, len(scopeDetails)*2)
idGen := didgen.NewDomainIdGenerator(&models.ArgocdApplication{})
for _, scopeDetail := range scopeDetails {
application, scopeConfig := scopeDetail.Scope, scopeDetail.ScopeConfig
scopes = append(scopes, application)
if scopeConfig == nil {
continue
}
entities := scopeConfig.Entities
if len(entities) == 0 {
entities = plugin.DOMAIN_TYPES
}
if utils.StringsContains(entities, plugin.DOMAIN_TYPE_CICD) {
scopeId := idGen.Generate(connection.ID, application.Name)
scopes = append(scopes, &devops.CicdScope{
DomainEntity: domainlayer.DomainEntity{Id: scopeId},
Name: application.Name,
})
}
}
return scopes, nil
}
206 changes: 206 additions & 0 deletions backend/plugins/argocd/api/connection_api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
/*
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 api

import (
"context"

"net/http"
"net/url"

"github.com/apache/incubator-devlake/core/errors"
"github.com/apache/incubator-devlake/core/plugin"
"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
"github.com/apache/incubator-devlake/plugins/argocd/models"
"github.com/apache/incubator-devlake/server/api/shared"
)

type ArgocdTestConnResponse struct {
shared.ApiBody
Connection *models.ArgocdConn
}

func testConnection(ctx context.Context, connection models.ArgocdConn) (*ArgocdTestConnResponse, errors.Error) {
// validate
if vld != nil {
if err := vld.Struct(connection); err != nil {
return nil, errors.Default.Wrap(err, "error validating target")
}
}
apiClient, err := api.NewApiClientFromConnection(ctx, basicRes, &connection)
if err != nil {
return nil, err
}

// Test ArgoCD API by listing applications
query := url.Values{}
res, err := apiClient.Get("applications", query, nil)
if err != nil {
return nil, err
}

if res.StatusCode == http.StatusUnauthorized {
return nil, errors.HttpStatus(http.StatusBadRequest).New("StatusUnauthorized error - check your token")
}

if res.StatusCode == http.StatusForbidden {
return nil, errors.BadInput.New("token lacks required permissions")
}

connection = connection.Sanitize()
body := ArgocdTestConnResponse{}
body.Success = true
body.Message = "success"
body.Connection = &connection

return &body, nil
}

// TestConnection test argocd connection
// @Summary test argocd connection
// @Description Test ArgoCD Connection
// @Tags plugins/argocd
// @Param body body models.ArgocdConn true "json body"
// @Success 200 {object} ArgocdTestConnResponse "Success"
// @Failure 400 {string} errcode.Error "Bad Request"
// @Failure 500 {string} errcode.Error "Internal Error"
// @Router /plugins/argocd/test [POST]
func TestConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
var err errors.Error
var connection models.ArgocdConn
if err = api.Decode(input.Body, &connection, vld); err != nil {
return nil, err
}
result, err := testConnection(context.TODO(), connection)
if err != nil {
return nil, plugin.WrapTestConnectionErrResp(basicRes, err)
}
return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, nil
}

// TestExistingConnection test argocd connection
// @Summary test argocd connection
// @Description Test ArgoCD Connection
// @Tags plugins/argocd
// @Param connectionId path int true "connection ID"
// @Success 200 {object} ArgocdTestConnResponse "Success"
// @Failure 400 {string} errcode.Error "Bad Request"
// @Failure 500 {string} errcode.Error "Internal Error"
// @Router /plugins/argocd/connections/{connectionId}/test [POST]
func TestExistingConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
connection, err := dsHelper.ConnApi.GetMergedConnection(input)
if err != nil {
return nil, errors.Convert(err)
}
if result, err := testConnection(context.TODO(), connection.ArgocdConn); err != nil {
return nil, plugin.WrapTestConnectionErrResp(basicRes, err)
} else {
return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, nil
}
}

// @Summary create argocd connection
// @Description Create ArgoCD connection
// @Tags plugins/argocd
// @Param body body models.ArgocdConnection true "json body"
// @Success 200 {object} models.ArgocdConnection
// @Failure 400 {string} errcode.Error "Bad Request"
// @Failure 500 {string} errcode.Error "Internal Error"
// @Router /plugins/argocd/connections [POST]
func PostConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
out, err := dsHelper.ConnApi.Post(input)
if err != nil {
return nil, err
}
connId := out.Body.(*models.ArgocdConnection).ID
_, _ = CreateDefaultScopeConfig(connId)
return out, nil
}

// @Summary patch argocd connection
// @Description Patch ArgoCD connection
// @Tags plugins/argocd
// @Param body body models.ArgocdConnection true "json body"
// @Success 200 {object} models.ArgocdConnection
// @Failure 400 {string} errcode.Error "Bad Request"
// @Failure 500 {string} errcode.Error "Internal Error"
// @Router /plugins/argocd/connections/{connectionId} [PATCH]
func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
return dsHelper.ConnApi.Patch(input)
}

// @Summary delete an argocd connection
// @Description Delete an ArgoCD connection
// @Tags plugins/argocd
// @Success 200 {object} models.ArgocdConnection
// @Failure 400 {string} errcode.Error "Bad Request"
// @Failure 409 {object} srvhelper.DsRefs "References exist to this connection"
// @Failure 500 {string} errcode.Error "Internal Error"
// @Router /plugins/argocd/connections/{connectionId} [DELETE]
func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
return dsHelper.ConnApi.Delete(input)
}

// @Summary get all argocd connections
// @Description Get all ArgoCD connections
// @Tags plugins/argocd
// @Success 200 {object} []models.ArgocdConnection
// @Failure 400 {string} errcode.Error "Bad Request"
// @Failure 500 {string} errcode.Error "Internal Error"
// @Router /plugins/argocd/connections [GET]
func ListConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
return dsHelper.ConnApi.GetAll(input)
}

// @Summary get argocd connection detail
// @Description Get ArgoCD connection detail
// @Tags plugins/argocd

// CreateDefaultScopeConfig ensures a default scope config exists
func CreateDefaultScopeConfig(connectionId uint64) (*models.ArgocdScopeConfig, errors.Error) {
if dsHelper.ScopeConfigSrv == nil {
return nil, nil
}
existing, _ := dsHelper.ScopeConfigSrv.GetAllByConnectionId(connectionId)
if len(existing) > 0 {
return existing[0], nil
}
cfg := &models.ArgocdScopeConfig{
ScopeConfig: models.ArgocdScopeConfig{}.ScopeConfig, // zero
DeploymentPattern: ".*",
ProductionPattern: "(?i)(prod|production)",
EnvNamePattern: "(?i)prod(.*)",
}
cfg.ConnectionId = connectionId
cfg.Name = "default"
cfg.Entities = []string{"CICD"}
err := dsHelper.ScopeConfigSrv.Create(cfg)
if err != nil {
return nil, err
}
return cfg, nil
}

// @Success 200 {object} models.ArgocdConnection
// @Failure 400 {string} errcode.Error "Bad Request"
// @Failure 500 {string} errcode.Error "Internal Error"

// @Router /plugins/argocd/connections/{connectionId} [GET]
func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
return dsHelper.ConnApi.GetDetail(input)
}
Loading
Loading