Skip to content

Commit 4e73c3e

Browse files
committed
feat(plugins): initial implementation of argocd
This commit implements a new ArgoCD plugin that enables deployment frequency tracking and DORA metrics calculation by collecting sync operation data from ArgoCD applications. It covers rollouts, deployments, replicaset, etc and are sorted per project. Included is also a grafana dashboard to show simple statistics for the applications. Since I used the .devcontainers, I also updated the version or else mockery would not work. The sorting of the plugins was also updated to keep them in order. IMPORTANT: The applications in ArgoCD must be synced at least once for the plugin to collect data. Importing them via an application set does not count as a sync operation. #5207
1 parent 8938b9e commit 4e73c3e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+4034
-24
lines changed

.devcontainer/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

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

1818
ENV PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/lib:/usr/local/lib/pkgconfig
1919
ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib
20-
ENV DEVLAKE_PLUGINS=bamboo,bitbucket,circleci,customize,dora,gitextractor,github,github_graphql,gitlab,jenkins,jira,org,pagerduty,refdiff,slack,sonarqube,trello,webhook
20+
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
2121

2222
RUN apt-get update -y
2323
RUN apt-get install pkg-config python3-dev default-libmysqlclient-dev build-essential libpq-dev cmake -y

.devcontainer/devcontainer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,4 @@
4545
}
4646
},
4747
"remoteUser": "root"
48-
}
48+
}

backend/plugins/argocd/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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+
Please see details in the [Apache DevLake website](https://devlake.apache.org/docs/Plugins/argocd)
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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/models/domainlayer"
24+
"github.com/apache/incubator-devlake/core/models/domainlayer/devops"
25+
"github.com/apache/incubator-devlake/core/models/domainlayer/didgen"
26+
"github.com/apache/incubator-devlake/core/plugin"
27+
"github.com/apache/incubator-devlake/core/utils"
28+
"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
29+
"github.com/apache/incubator-devlake/helpers/srvhelper"
30+
"github.com/apache/incubator-devlake/plugins/argocd/models"
31+
"github.com/apache/incubator-devlake/plugins/argocd/tasks"
32+
)
33+
34+
func MakeDataSourcePipelinePlanV200(
35+
subtaskMetas []plugin.SubTaskMeta,
36+
connectionId uint64,
37+
bpScopes []*coreModels.BlueprintScope,
38+
) (coreModels.PipelinePlan, []plugin.Scope, errors.Error) {
39+
connection, err := dsHelper.ConnSrv.FindByPk(connectionId)
40+
if err != nil {
41+
return nil, nil, err
42+
}
43+
scopeDetails, err := dsHelper.ScopeApi.MapScopeDetails(connectionId, bpScopes)
44+
if err != nil {
45+
// attempt auto-create missing scopes for blueprint (only name known)
46+
cfg, _ := CreateDefaultScopeConfig(connectionId)
47+
for _, bs := range bpScopes {
48+
if _, findErr := dsHelper.ScopeSrv.ModelSrvHelper.FindByPk(connectionId, bs.ScopeId); findErr != nil {
49+
app := &models.ArgocdApplication{}
50+
app.ConnectionId = connectionId
51+
app.Name = bs.ScopeId
52+
if cfg != nil {
53+
app.ScopeConfigId = cfg.ID
54+
}
55+
_ = dsHelper.ScopeSrv.ModelSrvHelper.CreateOrUpdate(app)
56+
}
57+
}
58+
scopeDetails, err = dsHelper.ScopeApi.MapScopeDetails(connectionId, bpScopes)
59+
if err != nil {
60+
return nil, nil, err
61+
}
62+
}
63+
64+
plan, err := makeDataSourcePipelinePlanV200(subtaskMetas, scopeDetails, connection)
65+
if err != nil {
66+
return nil, nil, err
67+
}
68+
scopes, err := makeScopesV200(scopeDetails, connection)
69+
if err != nil {
70+
return nil, nil, err
71+
}
72+
73+
return plan, scopes, nil
74+
}
75+
76+
func makeDataSourcePipelinePlanV200(
77+
subtaskMetas []plugin.SubTaskMeta,
78+
scopeDetails []*srvhelper.ScopeDetail[models.ArgocdApplication, models.ArgocdScopeConfig],
79+
connection *models.ArgocdConnection,
80+
) (coreModels.PipelinePlan, errors.Error) {
81+
plan := make(coreModels.PipelinePlan, len(scopeDetails))
82+
for i, scopeDetail := range scopeDetails {
83+
application := scopeDetail.Scope
84+
scopeConfig := scopeDetail.ScopeConfig
85+
if scopeConfig == nil {
86+
scopeConfig = &models.ArgocdScopeConfig{}
87+
}
88+
stage := plan[i]
89+
if stage == nil {
90+
stage = coreModels.PipelineStage{}
91+
}
92+
scopeConfigId := application.ScopeConfigId
93+
if scopeConfig != nil && scopeConfig.ID != 0 {
94+
scopeConfigId = scopeConfig.ID
95+
}
96+
task, err := api.MakePipelinePlanTask(
97+
"argocd",
98+
subtaskMetas,
99+
scopeConfig.Entities,
100+
tasks.ArgocdOptions{
101+
ConnectionId: connection.ID,
102+
ApplicationName: application.Name,
103+
ScopeConfigId: scopeConfigId,
104+
},
105+
)
106+
if err != nil {
107+
return nil, err
108+
}
109+
stage = append(stage, task)
110+
plan[i] = stage
111+
}
112+
113+
return plan, nil
114+
}
115+
116+
func makeScopesV200(
117+
scopeDetails []*srvhelper.ScopeDetail[models.ArgocdApplication, models.ArgocdScopeConfig],
118+
connection *models.ArgocdConnection,
119+
) ([]plugin.Scope, errors.Error) {
120+
scopes := make([]plugin.Scope, 0, len(scopeDetails)*2)
121+
idGen := didgen.NewDomainIdGenerator(&models.ArgocdApplication{})
122+
for _, scopeDetail := range scopeDetails {
123+
application, scopeConfig := scopeDetail.Scope, scopeDetail.ScopeConfig
124+
scopes = append(scopes, application)
125+
if scopeConfig == nil {
126+
continue
127+
}
128+
entities := scopeConfig.Entities
129+
if len(entities) == 0 {
130+
entities = plugin.DOMAIN_TYPES
131+
}
132+
if utils.StringsContains(entities, plugin.DOMAIN_TYPE_CICD) {
133+
scopeId := idGen.Generate(connection.ID, application.Name)
134+
scopes = append(scopes, &devops.CicdScope{
135+
DomainEntity: domainlayer.DomainEntity{Id: scopeId},
136+
Name: application.Name,
137+
})
138+
}
139+
}
140+
return scopes, nil
141+
}
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
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+
"context"
22+
23+
"net/http"
24+
"net/url"
25+
26+
"github.com/apache/incubator-devlake/core/errors"
27+
"github.com/apache/incubator-devlake/core/plugin"
28+
"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
29+
"github.com/apache/incubator-devlake/plugins/argocd/models"
30+
"github.com/apache/incubator-devlake/server/api/shared"
31+
)
32+
33+
type ArgocdTestConnResponse struct {
34+
shared.ApiBody
35+
Connection *models.ArgocdConn
36+
}
37+
38+
func testConnection(ctx context.Context, connection models.ArgocdConn) (*ArgocdTestConnResponse, errors.Error) {
39+
// validate
40+
if vld != nil {
41+
if err := vld.Struct(connection); err != nil {
42+
return nil, errors.Default.Wrap(err, "error validating target")
43+
}
44+
}
45+
apiClient, err := api.NewApiClientFromConnection(ctx, basicRes, &connection)
46+
if err != nil {
47+
return nil, err
48+
}
49+
50+
// Test ArgoCD API by listing applications
51+
query := url.Values{}
52+
res, err := apiClient.Get("applications", query, nil)
53+
if err != nil {
54+
return nil, err
55+
}
56+
57+
if res.StatusCode == http.StatusUnauthorized {
58+
return nil, errors.HttpStatus(http.StatusBadRequest).New("StatusUnauthorized error - check your token")
59+
}
60+
61+
if res.StatusCode == http.StatusForbidden {
62+
return nil, errors.BadInput.New("token lacks required permissions")
63+
}
64+
65+
connection = connection.Sanitize()
66+
body := ArgocdTestConnResponse{}
67+
body.Success = true
68+
body.Message = "success"
69+
body.Connection = &connection
70+
71+
return &body, nil
72+
}
73+
74+
// TestConnection test argocd connection
75+
// @Summary test argocd connection
76+
// @Description Test ArgoCD Connection
77+
// @Tags plugins/argocd
78+
// @Param body body models.ArgocdConn true "json body"
79+
// @Success 200 {object} ArgocdTestConnResponse "Success"
80+
// @Failure 400 {string} errcode.Error "Bad Request"
81+
// @Failure 500 {string} errcode.Error "Internal Error"
82+
// @Router /plugins/argocd/test [POST]
83+
func TestConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
84+
var err errors.Error
85+
var connection models.ArgocdConn
86+
if err = api.Decode(input.Body, &connection, vld); err != nil {
87+
return nil, err
88+
}
89+
result, err := testConnection(context.TODO(), connection)
90+
if err != nil {
91+
return nil, plugin.WrapTestConnectionErrResp(basicRes, err)
92+
}
93+
return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, nil
94+
}
95+
96+
// TestExistingConnection test argocd connection
97+
// @Summary test argocd connection
98+
// @Description Test ArgoCD Connection
99+
// @Tags plugins/argocd
100+
// @Param connectionId path int true "connection ID"
101+
// @Success 200 {object} ArgocdTestConnResponse "Success"
102+
// @Failure 400 {string} errcode.Error "Bad Request"
103+
// @Failure 500 {string} errcode.Error "Internal Error"
104+
// @Router /plugins/argocd/connections/{connectionId}/test [POST]
105+
func TestExistingConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
106+
connection, err := dsHelper.ConnApi.GetMergedConnection(input)
107+
if err != nil {
108+
return nil, errors.Convert(err)
109+
}
110+
if result, err := testConnection(context.TODO(), connection.ArgocdConn); err != nil {
111+
return nil, plugin.WrapTestConnectionErrResp(basicRes, err)
112+
} else {
113+
return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, nil
114+
}
115+
}
116+
117+
// @Summary create argocd connection
118+
// @Description Create ArgoCD connection
119+
// @Tags plugins/argocd
120+
// @Param body body models.ArgocdConnection true "json body"
121+
// @Success 200 {object} models.ArgocdConnection
122+
// @Failure 400 {string} errcode.Error "Bad Request"
123+
// @Failure 500 {string} errcode.Error "Internal Error"
124+
// @Router /plugins/argocd/connections [POST]
125+
func PostConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
126+
out, err := dsHelper.ConnApi.Post(input)
127+
if err != nil {
128+
return nil, err
129+
}
130+
connId := out.Body.(*models.ArgocdConnection).ID
131+
_, _ = CreateDefaultScopeConfig(connId)
132+
return out, nil
133+
}
134+
135+
// @Summary patch argocd connection
136+
// @Description Patch ArgoCD connection
137+
// @Tags plugins/argocd
138+
// @Param body body models.ArgocdConnection true "json body"
139+
// @Success 200 {object} models.ArgocdConnection
140+
// @Failure 400 {string} errcode.Error "Bad Request"
141+
// @Failure 500 {string} errcode.Error "Internal Error"
142+
// @Router /plugins/argocd/connections/{connectionId} [PATCH]
143+
func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
144+
return dsHelper.ConnApi.Patch(input)
145+
}
146+
147+
// @Summary delete an argocd connection
148+
// @Description Delete an ArgoCD connection
149+
// @Tags plugins/argocd
150+
// @Success 200 {object} models.ArgocdConnection
151+
// @Failure 400 {string} errcode.Error "Bad Request"
152+
// @Failure 409 {object} srvhelper.DsRefs "References exist to this connection"
153+
// @Failure 500 {string} errcode.Error "Internal Error"
154+
// @Router /plugins/argocd/connections/{connectionId} [DELETE]
155+
func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
156+
return dsHelper.ConnApi.Delete(input)
157+
}
158+
159+
// @Summary get all argocd connections
160+
// @Description Get all ArgoCD connections
161+
// @Tags plugins/argocd
162+
// @Success 200 {object} []models.ArgocdConnection
163+
// @Failure 400 {string} errcode.Error "Bad Request"
164+
// @Failure 500 {string} errcode.Error "Internal Error"
165+
// @Router /plugins/argocd/connections [GET]
166+
func ListConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
167+
return dsHelper.ConnApi.GetAll(input)
168+
}
169+
170+
// @Summary get argocd connection detail
171+
// @Description Get ArgoCD connection detail
172+
// @Tags plugins/argocd
173+
174+
// CreateDefaultScopeConfig ensures a default scope config exists
175+
func CreateDefaultScopeConfig(connectionId uint64) (*models.ArgocdScopeConfig, errors.Error) {
176+
if dsHelper.ScopeConfigSrv == nil {
177+
return nil, nil
178+
}
179+
existing, _ := dsHelper.ScopeConfigSrv.GetAllByConnectionId(connectionId)
180+
if len(existing) > 0 {
181+
return existing[0], nil
182+
}
183+
cfg := &models.ArgocdScopeConfig{
184+
ScopeConfig: models.ArgocdScopeConfig{}.ScopeConfig, // zero
185+
DeploymentPattern: ".*",
186+
ProductionPattern: "(?i)(prod|production)",
187+
EnvNamePattern: "(?i)prod(.*)",
188+
}
189+
cfg.ConnectionId = connectionId
190+
cfg.Name = "default"
191+
cfg.Entities = []string{"CICD"}
192+
err := dsHelper.ScopeConfigSrv.Create(cfg)
193+
if err != nil {
194+
return nil, err
195+
}
196+
return cfg, nil
197+
}
198+
199+
// @Success 200 {object} models.ArgocdConnection
200+
// @Failure 400 {string} errcode.Error "Bad Request"
201+
// @Failure 500 {string} errcode.Error "Internal Error"
202+
203+
// @Router /plugins/argocd/connections/{connectionId} [GET]
204+
func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
205+
return dsHelper.ConnApi.GetDetail(input)
206+
}

0 commit comments

Comments
 (0)