Skip to content

Commit 2b76230

Browse files
committed
Support two way to fetch matching ArgoCD apps from a Telefonistka
component the telefonistka dedicated sha1 label and the ArgoCD native `manifest-generate-paths` annotation
1 parent 2a5adc2 commit 2b76230

File tree

8 files changed

+295
-23
lines changed

8 files changed

+295
-23
lines changed

go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ toolchain go1.22.1
66

77
require (
88
github.com/alexliesenfeld/health v0.8.0
9-
github.com/argoproj/argo-cd/v2 v2.11.0-rc1
10-
github.com/argoproj/gitops-engine v0.7.1-0.20240411122334-1ade3a199867
9+
github.com/argoproj/argo-cd/v2 v2.11.2
10+
github.com/argoproj/gitops-engine v0.7.1-0.20240416142647-fbecbb86e412
1111
github.com/bradleyfalzon/ghinstallation/v2 v2.10.0
1212
github.com/go-test/deep v1.1.0
1313
github.com/google/go-github/v52 v52.0.0
@@ -77,6 +77,7 @@ require (
7777
github.com/gogo/protobuf v1.3.2 // indirect
7878
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
7979
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
80+
github.com/golang/mock v1.6.0 // indirect
8081
github.com/golang/protobuf v1.5.4 // indirect
8182
github.com/google/btree v1.1.2 // indirect
8283
github.com/google/gnostic v0.7.0 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,8 +651,12 @@ github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4x
651651
github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU=
652652
github.com/argoproj/argo-cd/v2 v2.11.0-rc1 h1:VCjpw0bwPcULNJ/FG8BnIyeesyOpT+1MUWuXPskbsWQ=
653653
github.com/argoproj/argo-cd/v2 v2.11.0-rc1/go.mod h1:/KySYrOzPQupCh7E1pzg6011p4AaRLszbUtkaWyATTU=
654+
github.com/argoproj/argo-cd/v2 v2.11.2 h1:NygNrTFIMWUe1b48ddUuH+q2vRTHB+dFk3NcErx6GcM=
655+
github.com/argoproj/argo-cd/v2 v2.11.2/go.mod h1:nUOZqAT9f3GewdG/dzYgrpwqOSMj5ukoWw4yAV2/WXA=
654656
github.com/argoproj/gitops-engine v0.7.1-0.20240411122334-1ade3a199867 h1:zMATM3uzAQHBLJ142MEGrZ4+3+xsXT36hzB1Dj2jptE=
655657
github.com/argoproj/gitops-engine v0.7.1-0.20240411122334-1ade3a199867/go.mod h1:gWE8uROi7hIkWGNAVM+8FWkMfo0vZ03SLx/aFw/DBzg=
658+
github.com/argoproj/gitops-engine v0.7.1-0.20240416142647-fbecbb86e412 h1:je2wJpWtaoS55mA5MBPCeDnKMeF42pkxO9Oa5KbWrdg=
659+
github.com/argoproj/gitops-engine v0.7.1-0.20240416142647-fbecbb86e412/go.mod h1:gWE8uROi7hIkWGNAVM+8FWkMfo0vZ03SLx/aFw/DBzg=
656660
github.com/argoproj/pkg v0.13.7-0.20230626144333-d56162821bd1 h1:qsHwwOJ21K2Ao0xPju1sNuqphyMnMYkyB3ZLoLtxWpo=
657661
github.com/argoproj/pkg v0.13.7-0.20230626144333-d56162821bd1/go.mod h1:CZHlkyAD1/+FbEn6cB2DQTj48IoLGvEYsWEvtzP3238=
658662
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=

internal/pkg/argocd/argocd.go

Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import (
77
"encoding/json"
88
"fmt"
99
"os"
10+
"path/filepath"
1011
"strconv"
12+
"strings"
1113

1214
cmdutil "github.com/argoproj/argo-cd/v2/cmd/util"
1315
"github.com/argoproj/argo-cd/v2/pkg/apiclient"
@@ -151,42 +153,102 @@ func createArgoCdClient() (apiclient.Client, error) {
151153
return clientset, nil
152154
}
153155

154-
func generateDiffOfAComponent(ctx context.Context, componentPath string, prBranch string, repo string, appIf application.ApplicationServiceClient, projIf projectpkg.ProjectServiceClient, argoSettings *settings.Settings) (componentDiffResult DiffResult) {
155-
componentDiffResult.ComponentPath = componentPath
156-
156+
// This function is used to find an ArgoCD application by the SHA1 label of the component path its supposed to avoid perfromance issues with the "manifest-generate-paths" annotation method with requires pulling all ArgoCD applications(!) on every PR event.
157+
// The SHA1 label is assumed to be populated by the ApplicationSet controller(or apps of apps or similar).
158+
func findArgocdAppBySHA1Label(ctx context.Context, componentPath string, repo string, appIf application.ApplicationServiceClient) (app *argoappv1.Application, err error) {
157159
// Calculate sha1 of component path to use in a label selector
158160
cPathBa := []byte(componentPath)
159161
hasher := sha1.New() //nolint:gosec // G505: Blocklisted import crypto/sha1: weak cryptographic primitive (gosec), this is not a cryptographic use case
160162
hasher.Write(cPathBa)
161163
componentPathSha1 := hex.EncodeToString(hasher.Sum(nil))
162-
163-
// Find ArgoCD application by the path SHA1 label selector and repo name
164-
// That label is assumed to be pupulated by the ApplicationSet controller(or apps of apps or similar).
165164
labelSelector := fmt.Sprintf("telefonistka.io/component-path-sha1=%s", componentPathSha1)
166165
appLabelQuery := application.ApplicationQuery{
167166
Selector: &labelSelector,
168167
Repo: &repo,
169168
}
170169
foundApps, err := appIf.List(ctx, &appLabelQuery)
171170
if err != nil {
172-
componentDiffResult.DiffError = err
173-
return componentDiffResult
171+
return nil, err
174172
}
175173
if len(foundApps.Items) == 0 {
176-
componentDiffResult.DiffError = fmt.Errorf("No ArgoCD application found for component path %s(repo %s), used this label selector: %s", componentPath, repo, labelSelector)
174+
return nil, fmt.Errorf("No ArgoCD application found for component path sha1 %s(repo %s), used this label selector: %s", componentPathSha1, repo, labelSelector)
175+
}
176+
177+
// we expect only one app with this label and repo selectors
178+
return &foundApps.Items[0], nil
179+
}
180+
181+
// This is the default method to find an ArgoCD application by the manifest-generate-paths annotation.
182+
// It assume the ArgoCD (optional) manifest-generate-paths annotation is set on all relevent apps.
183+
// Notice that this method include a full list all ArgoCD applications in the repo, this could be a performance issue if there are many apps in the repo.
184+
func findArgocdAppByManifestPathAnnotation(ctx context.Context, componentPath string, repo string, appIf application.ApplicationServiceClient) (app *argoappv1.Application, err error) {
185+
//argocd.argoproj.io/manifest-generate-paths
186+
appQuery := application.ApplicationQuery{
187+
Repo: &repo,
188+
}
189+
// TODO Instrument this, we might have a lot of apps in a repo, performance might be an issue
190+
allRepoApps, err := appIf.List(ctx, &appQuery)
191+
if err != nil {
192+
return nil, err
193+
}
194+
for _, app := range allRepoApps.Items {
195+
// Check if the app has the annotation
196+
// https://argo-cd.readthedocs.io/en/stable/operator-manual/high_availability/#manifest-paths-annotation
197+
// Consider the annotation content can a semi-colon separated list of paths, an absolute path or a relative path(start with a ".") and the manifest-paths-annotation could be a subpath of componentPath.
198+
// We need to check if the annotation is a subpath of componentPath
199+
200+
appManifestPathsAnnotation := app.Annotations["argocd.argoproj.io/manifest-generate-paths"]
201+
202+
for _, manifetsPathElement := range strings.Split(appManifestPathsAnnotation, ";") {
203+
// if `manifest-generate-paths` element starts with a "." it is a relative path(relative to repo root), we need to join it with the app source path
204+
if strings.HasPrefix(manifetsPathElement, ".") {
205+
manifetsPathElement = filepath.Join(app.Spec.Source.Path, manifetsPathElement)
206+
}
207+
208+
// Checking is componentPath is a subpath of the manifetsPathElement
209+
// Using filepath.Rel solves all kinds of path issues, like double slashes, etc.
210+
rel, err := filepath.Rel(manifetsPathElement, componentPath)
211+
if !strings.HasPrefix(rel, "..") && err == nil {
212+
log.Debugf("Found app %s with manifest-generate-paths(\"%s\") annotation that matches %s", app.Name, appManifestPathsAnnotation, componentPath)
213+
return &app, nil
214+
}
215+
216+
}
217+
218+
}
219+
return nil, fmt.Errorf("No ArgoCD application found with manifest-generate-paths annotation that matches %s(looked at repo %s, checked %v apps) ", componentPath, repo, len(allRepoApps.Items))
220+
}
221+
222+
func generateDiffOfAComponent(ctx context.Context, componentPath string, prBranch string, repo string, appIf application.ApplicationServiceClient, projIf projectpkg.ProjectServiceClient, argoSettings *settings.Settings, usaSHALabelForArgoDicovery bool) (componentDiffResult DiffResult) {
223+
componentDiffResult.ComponentPath = componentPath
224+
225+
// Find ArgoCD application by the path SHA1 label selector and repo name
226+
// At the moment we assume one to one mapping between Telefonistka components and ArgoCD application
227+
228+
var foundApp *argoappv1.Application
229+
var err error
230+
if usaSHALabelForArgoDicovery {
231+
foundApp, err = findArgocdAppBySHA1Label(ctx, componentPath, repo, appIf)
232+
} else {
233+
foundApp, err = findArgocdAppByManifestPathAnnotation(ctx, componentPath, repo, appIf)
234+
235+
}
236+
if err != nil {
237+
componentDiffResult.DiffError = err
177238
return componentDiffResult
178239
}
179240

180241
// Get the application and its resources, resources are the live state of the application objects.
242+
// The 2nd "app fetch" is needed for the "refreshTypeHArd", we don't want to do that to non-relevant apps"
181243
refreshType := string(argoappv1.RefreshTypeHard)
182244
appNameQuery := application.ApplicationQuery{
183-
Name: &foundApps.Items[0].Name, // we expect only one app with this label and repo selectors
245+
Name: &foundApp.Name, // we expect only one app with this label and repo selectors
184246
Refresh: &refreshType,
185247
}
186248
app, err := appIf.Get(ctx, &appNameQuery)
187249
if err != nil {
188250
componentDiffResult.DiffError = err
189-
log.Errorf("Error getting app %s: %v", foundApps.Items[0].Name, err)
251+
log.Errorf("Error getting app %s: %v", foundApp.Name, err)
190252
return componentDiffResult
191253
}
192254
componentDiffResult.ArgoCdAppName = app.Name
@@ -232,7 +294,7 @@ func generateDiffOfAComponent(ctx context.Context, componentPath string, prBranc
232294
}
233295

234296
// GenerateDiffOfChangedComponents generates diff of changed components
235-
func GenerateDiffOfChangedComponents(ctx context.Context, componentPathList []string, prBranch string, repo string) (hasComponentDiff bool, hasComponentDiffErrors bool, diffResults []DiffResult, err error) {
297+
func GenerateDiffOfChangedComponents(ctx context.Context, componentPathList []string, prBranch string, repo string, usaSHALabelForArgoDicovery bool) (hasComponentDiff bool, hasComponentDiffErrors bool, diffResults []DiffResult, err error) {
236298
hasComponentDiff = false
237299
hasComponentDiffErrors = false
238300
// env var should be centralized
@@ -264,7 +326,7 @@ func GenerateDiffOfChangedComponents(ctx context.Context, componentPathList []st
264326
}
265327

266328
for _, componentPath := range componentPathList {
267-
currentDiffResult := generateDiffOfAComponent(ctx, componentPath, prBranch, repo, appIf, projIf, argoSettings)
329+
currentDiffResult := generateDiffOfAComponent(ctx, componentPath, prBranch, repo, appIf, projIf, argoSettings, usaSHALabelForArgoDicovery)
268330
if currentDiffResult.DiffError != nil {
269331
hasComponentDiffErrors = true
270332
err = currentDiffResult.DiffError

internal/pkg/argocd/argocd_test.go

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
package argocd
2+
3+
// @Title
4+
// @Description
5+
// @Author
6+
// @Update
7+
8+
import (
9+
"context"
10+
"testing"
11+
12+
argoappv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
13+
"github.com/golang/mock/gomock"
14+
argo_app_mock "github.com/wayfair-incubator/telefonistka/mocks/argocd"
15+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16+
)
17+
18+
func TestFindArgocdAppBySHA1Label(t *testing.T) {
19+
// Here the filtering is done on the ArgoCD server side, so we are just testing the function returns a app
20+
t.Parallel()
21+
ctx := context.Background()
22+
ctrl := gomock.NewController(t)
23+
defer ctrl.Finish()
24+
mockApplicationClient := argo_app_mock.NewMockApplicationServiceClient(ctrl)
25+
expectedResponse := &argoappv1.ApplicationList{
26+
Items: []argoappv1.Application{
27+
{
28+
ObjectMeta: metav1.ObjectMeta{
29+
Labels: map[string]string{
30+
"telefonistka.io/component-path-sha1": "111111",
31+
},
32+
Name: "right-app",
33+
},
34+
},
35+
},
36+
}
37+
38+
mockApplicationClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(expectedResponse, nil)
39+
40+
app, err := findArgocdAppBySHA1Label(ctx, "random/path", "some-repo", mockApplicationClient)
41+
if err != nil {
42+
t.Errorf("Error: %v", err)
43+
}
44+
if app.Name != "right-app" {
45+
t.Errorf("App name is not right-app")
46+
}
47+
}
48+
49+
func TestFindArgocdAppByPathAnnotation(t *testing.T) {
50+
t.Parallel()
51+
ctx := context.Background()
52+
ctrl := gomock.NewController(t)
53+
defer ctrl.Finish()
54+
mockApplicationClient := argo_app_mock.NewMockApplicationServiceClient(ctrl)
55+
expectedResponse := &argoappv1.ApplicationList{
56+
Items: []argoappv1.Application{
57+
{
58+
ObjectMeta: metav1.ObjectMeta{
59+
Annotations: map[string]string{
60+
"argocd.argoproj.io/manifest-generate-paths": "wrong/path/",
61+
},
62+
Name: "wrong-app",
63+
},
64+
},
65+
{
66+
ObjectMeta: metav1.ObjectMeta{
67+
Annotations: map[string]string{
68+
"argocd.argoproj.io/manifest-generate-paths": "right/path/",
69+
},
70+
Name: "right-app",
71+
},
72+
},
73+
},
74+
}
75+
76+
mockApplicationClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(expectedResponse, nil)
77+
78+
apps, err := findArgocdAppByManifestPathAnnotation(ctx, "right/path", "some-repo", mockApplicationClient)
79+
if err != nil {
80+
t.Errorf("Error: %v", err)
81+
}
82+
t.Logf("apps: %v", apps)
83+
84+
}
85+
86+
// Here I'm testing a ";" delimted path annotation
87+
func TestFindArgocdAppByPathAnnotationSemiColon(t *testing.T) {
88+
t.Parallel()
89+
ctx := context.Background()
90+
ctrl := gomock.NewController(t)
91+
defer ctrl.Finish()
92+
mockApplicationClient := argo_app_mock.NewMockApplicationServiceClient(ctrl)
93+
expectedResponse := &argoappv1.ApplicationList{
94+
Items: []argoappv1.Application{
95+
{
96+
ObjectMeta: metav1.ObjectMeta{
97+
Annotations: map[string]string{
98+
"argocd.argoproj.io/manifest-generate-paths": "wrong/path/;wrong/path2/",
99+
},
100+
Name: "wrong-app",
101+
},
102+
},
103+
{ // This is the app we want to find - it has the right path as one of the elements in the annotation
104+
ObjectMeta: metav1.ObjectMeta{
105+
Annotations: map[string]string{
106+
"argocd.argoproj.io/manifest-generate-paths": "wrong/path/;right/path/",
107+
},
108+
Name: "right-app",
109+
},
110+
},
111+
},
112+
}
113+
114+
mockApplicationClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(expectedResponse, nil)
115+
116+
app, err := findArgocdAppByManifestPathAnnotation(ctx, "right/path", "some-repo", mockApplicationClient)
117+
if err != nil {
118+
t.Errorf("Error: %v", err)
119+
}
120+
if app.Name != "right-app" {
121+
t.Errorf("App name is not right-app")
122+
}
123+
124+
}
125+
126+
// Here I'm testing a "." path annotation - this is a special case where the path is relative to the repo root specified in the application .spec
127+
func TestFindArgocdAppByPathAnnotationRelative(t *testing.T) {
128+
t.Parallel()
129+
ctx := context.Background()
130+
ctrl := gomock.NewController(t)
131+
defer ctrl.Finish()
132+
mockApplicationClient := argo_app_mock.NewMockApplicationServiceClient(ctrl)
133+
expectedResponse := &argoappv1.ApplicationList{
134+
Items: []argoappv1.Application{
135+
{
136+
ObjectMeta: metav1.ObjectMeta{
137+
Annotations: map[string]string{
138+
"argocd.argoproj.io/manifest-generate-paths": ".",
139+
},
140+
Name: "right-app",
141+
},
142+
Spec: argoappv1.ApplicationSpec{
143+
Source: &argoappv1.ApplicationSource{
144+
RepoURL: "",
145+
Path: "right/path",
146+
},
147+
},
148+
},
149+
},
150+
}
151+
152+
mockApplicationClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(expectedResponse, nil)
153+
app, err := findArgocdAppByManifestPathAnnotation(ctx, "right/path", "some-repo", mockApplicationClient)
154+
if err != nil {
155+
t.Errorf("Error: %v", err)
156+
} else if app.Name != "right-app" {
157+
t.Errorf("App name is not right-app")
158+
}
159+
160+
}
161+
162+
// Here I'm testing a "." path annotation - this is a special case where the path is relative to the repo root specified in the application .spec
163+
func TestFindArgocdAppByPathAnnotationRelative2(t *testing.T) {
164+
t.Parallel()
165+
ctx := context.Background()
166+
ctrl := gomock.NewController(t)
167+
defer ctrl.Finish()
168+
mockApplicationClient := argo_app_mock.NewMockApplicationServiceClient(ctrl)
169+
expectedResponse := &argoappv1.ApplicationList{
170+
Items: []argoappv1.Application{
171+
{
172+
ObjectMeta: metav1.ObjectMeta{
173+
Annotations: map[string]string{
174+
"argocd.argoproj.io/manifest-generate-paths": "./path",
175+
},
176+
Name: "right-app",
177+
},
178+
Spec: argoappv1.ApplicationSpec{
179+
Source: &argoappv1.ApplicationSource{
180+
RepoURL: "",
181+
Path: "right/",
182+
},
183+
},
184+
},
185+
},
186+
}
187+
188+
mockApplicationClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(expectedResponse, nil)
189+
app, err := findArgocdAppByManifestPathAnnotation(ctx, "right/path", "some-repo", mockApplicationClient)
190+
if err != nil {
191+
t.Errorf("Error: %v", err)
192+
} else if app.Name != "right-app" {
193+
t.Errorf("App name is not right-app")
194+
}
195+
196+
}

internal/pkg/configuration/config.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,14 @@ type Config struct {
3535
PromotionPaths []PromotionPath `yaml:"promotionPaths"`
3636

3737
// Generic configuration
38-
PromtionPrLables []string `yaml:"promtionPRlables"`
39-
DryRunMode bool `yaml:"dryRunMode"`
40-
AutoApprovePromotionPrs bool `yaml:"autoApprovePromotionPrs"`
41-
ToggleCommitStatus map[string]string `yaml:"toggleCommitStatus"`
42-
WebhookEndpointRegexs []WebhookEndpointRegex `yaml:"webhookEndpointRegexs"`
43-
CommentArgocdDiffonPR bool `yaml:"commentArgocdDiffonPR"`
44-
AutoMergeNoDiffPRs bool `yaml:"autoMergeNoDiffPRs"`
38+
PromtionPrLables []string `yaml:"promtionPRlables"`
39+
DryRunMode bool `yaml:"dryRunMode"`
40+
AutoApprovePromotionPrs bool `yaml:"autoApprovePromotionPrs"`
41+
ToggleCommitStatus map[string]string `yaml:"toggleCommitStatus"`
42+
WebhookEndpointRegexs []WebhookEndpointRegex `yaml:"webhookEndpointRegexs"`
43+
CommentArgocdDiffonPR bool `yaml:"commentArgocdDiffonPR"`
44+
AutoMergeNoDiffPRs bool `yaml:"autoMergeNoDiffPRs"`
45+
UsaSHALabelForArgoDicovery bool `yaml:"usaSHALabelForArgoDicovery"`
4546
}
4647

4748
func ParseConfigFromYaml(y string) (*Config, error) {

0 commit comments

Comments
 (0)