Skip to content

Commit 2a96ff7

Browse files
Oded-Bhnnsgstfssn
andauthored
Allow setting argoCD revision to PR git Branch (#16)
* Allow setting argoCD revision to PR git branch * Triggering from checkbox event was written in a generic way for future proofing * Document new config key --------- Co-authored-by: Hannes Gustafsson <[email protected]>
1 parent 82d4fc2 commit 2a96ff7

File tree

7 files changed

+336
-77
lines changed

7 files changed

+336
-77
lines changed

docs/installation.md

+2
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ Configuration keys:
126126
|`commentArgocdDiffonPR`| Uses ArgoCD API to calculate expected changes to k8s state and comment the resulting "diff" as comment in the PR. Requires ARGOCD_* environment variables, see below. |
127127
|`autoMergeNoDiffPRs`| if true, Telefonistka will **merge** promotion PRs that are not expected to change the target clusters. Requires `commentArgocdDiffonPR` and possibly `autoApprovePromotionPrs`(depending on repo branch protection rules)|
128128
|`useSHALabelForArgoDicovery`| The default method for discovering relevant ArgoCD applications (for a PR) relies on fetching all applications in the repo and checking the `argocd.argoproj.io/manifest-generate-paths` **annotation**, this might cause a performance issue on a repo with a large number of ArgoCD applications. The alternative is to add SHA1 of the application path as a **label** and rely on ArgoCD server-side filtering, label name is `telefonistka.io/component-path-sha1`.|
129+
|`allowSyncArgoCDAppfromBranchPathRegex`| This controls which component(=ArgoCD apps) are allowed to be "applied" from a PR branch, by setting the ArgoCD application `Target Revision` to PR branch.|
129130
<!-- markdownlint-enable MD033 -->
130131

131132
Example:
@@ -173,6 +174,7 @@ dryRunMode: true
173174
autoApprovePromotionPrs: true
174175
commentArgocdDiffonPR: true
175176
autoMergeNoDiffPRs: true
177+
allowSyncArgoCDAppfromBranchPathRegex: '^workspace/.*$'
176178
toggleCommitStatus:
177179
override-terrafrom-pipeline: "github-action-terraform"
178180
```

internal/pkg/argocd/argocd.go

+92-38
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,18 @@ import (
2020
argoappv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
2121
argodiff "github.com/argoproj/argo-cd/v2/util/argo/diff"
2222
"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
23-
argoio "github.com/argoproj/argo-cd/v2/util/io"
2423
"github.com/argoproj/gitops-engine/pkg/sync/hook"
2524
"github.com/google/go-cmp/cmp"
2625
log "github.com/sirupsen/logrus"
2726
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2827
)
2928

29+
type argoCdClients struct {
30+
app application.ApplicationServiceClient
31+
project projectpkg.ProjectServiceClient
32+
setting settings.SettingsServiceClient
33+
}
34+
3035
// DiffElement struct to store diff element details, this represents a single k8s object
3136
type DiffElement struct {
3237
ObjectGroup string
@@ -51,25 +56,25 @@ type DiffResult struct {
5156
func generateArgocdAppDiff(ctx context.Context, app *argoappv1.Application, proj *argoappv1.AppProject, resources *application.ManagedResourcesResponse, argoSettings *settings.Settings, diffOptions *DifferenceOption) (foundDiffs bool, diffElements []DiffElement, err error) {
5257
liveObjs, err := cmdutil.LiveObjects(resources.Items)
5358
if err != nil {
54-
return false, nil, fmt.Errorf("Failed to get live objects: %v", err)
59+
return false, nil, fmt.Errorf("Failed to get live objects: %w", err)
5560
}
5661

5762
items := make([]objKeyLiveTarget, 0)
5863
var unstructureds []*unstructured.Unstructured
5964
for _, mfst := range diffOptions.res.Manifests {
6065
obj, err := argoappv1.UnmarshalToUnstructured(mfst)
6166
if err != nil {
62-
return false, nil, fmt.Errorf("Failed to unmarshal manifest: %v", err)
67+
return false, nil, fmt.Errorf("Failed to unmarshal manifest: %w", err)
6368
}
6469
unstructureds = append(unstructureds, obj)
6570
}
6671
groupedObjs, err := groupObjsByKey(unstructureds, liveObjs, app.Spec.Destination.Namespace)
6772
if err != nil {
68-
return false, nil, fmt.Errorf("Failed to group objects by key: %v", err)
73+
return false, nil, fmt.Errorf("Failed to group objects by key: %w", err)
6974
}
7075
items, err = groupObjsForDiff(resources, groupedObjs, items, argoSettings, app.InstanceName(argoSettings.ControllerNamespace), app.Spec.Destination.Namespace)
7176
if err != nil {
72-
return false, nil, fmt.Errorf("Failed to group objects for diff: %v", err)
77+
return false, nil, fmt.Errorf("Failed to group objects for diff: %w", err)
7378
}
7479

7580
for _, item := range items {
@@ -91,11 +96,11 @@ func generateArgocdAppDiff(ctx context.Context, app *argoappv1.Application, proj
9196
WithNoCache().
9297
Build()
9398
if err != nil {
94-
return false, nil, fmt.Errorf("Failed to build diff config: %v", err)
99+
return false, nil, fmt.Errorf("Failed to build diff config: %w", err)
95100
}
96101
diffRes, err := argodiff.StateDiff(item.live, item.target, diffConfig)
97102
if err != nil {
98-
return false, nil, fmt.Errorf("Failed to diff objects: %v", err)
103+
return false, nil, fmt.Errorf("Failed to diff objects: %w", err)
99104
}
100105

101106
if diffRes.Modified || item.target == nil || item.live == nil {
@@ -111,7 +116,7 @@ func generateArgocdAppDiff(ctx context.Context, app *argoappv1.Application, proj
111116
live = item.live
112117
err = json.Unmarshal(diffRes.PredictedLive, target)
113118
if err != nil {
114-
return false, nil, fmt.Errorf("Failed to unmarshal predicted live object: %v", err)
119+
return false, nil, fmt.Errorf("Failed to unmarshal predicted live object: %w", err)
115120
}
116121
} else {
117122
live = item.live
@@ -123,7 +128,7 @@ func generateArgocdAppDiff(ctx context.Context, app *argoappv1.Application, proj
123128

124129
diffElement.Diff, err = diffLiveVsTargetObject(live, target)
125130
if err != nil {
126-
return false, nil, fmt.Errorf("Failed to diff live objects: %v", err)
131+
return false, nil, fmt.Errorf("Failed to diff live objects: %w", err)
127132
}
128133
}
129134
diffElements = append(diffElements, diffElement)
@@ -144,7 +149,7 @@ func getEnv(key, fallback string) string {
144149
return fallback
145150
}
146151

147-
func createArgoCdClient() (apiclient.Client, error) {
152+
func createArgoCdClients() (ac argoCdClients, err error) {
148153
plaintext, _ := strconv.ParseBool(getEnv("ARGOCD_PLAINTEXT", "false"))
149154
insecure, _ := strconv.ParseBool(getEnv("ARGOCD_INSECURE", "false"))
150155

@@ -155,11 +160,26 @@ func createArgoCdClient() (apiclient.Client, error) {
155160
Insecure: insecure,
156161
}
157162

158-
clientset, err := apiclient.NewClient(opts)
163+
client, err := apiclient.NewClient(opts)
164+
if err != nil {
165+
return ac, fmt.Errorf("Error creating ArgoCD API client: %w", err)
166+
}
167+
168+
_, ac.app, err = client.NewApplicationClient()
169+
if err != nil {
170+
return ac, fmt.Errorf("Error creating ArgoCD app client: %w", err)
171+
}
172+
173+
_, ac.project, err = client.NewProjectClient()
174+
if err != nil {
175+
return ac, fmt.Errorf("Error creating ArgoCD project client: %w", err)
176+
}
177+
178+
_, ac.setting, err = client.NewSettingsClient()
159179
if err != nil {
160-
return nil, fmt.Errorf("Error creating ArgoCD API client: %v", err)
180+
return ac, fmt.Errorf("Error creating ArgoCD settings client: %w", err)
161181
}
162-
return clientset, nil
182+
return
163183
}
164184

165185
// findArgocdAppBySHA1Label finds an ArgoCD application by the SHA1 label of the component path it's supposed to avoid performance issues with the "manifest-generate-paths" annotation method which requires pulling all ArgoCD applications(!) on every PR event.
@@ -178,7 +198,7 @@ func findArgocdAppBySHA1Label(ctx context.Context, componentPath string, repo st
178198
}
179199
foundApps, err := appClient.List(ctx, &appLabelQuery)
180200
if err != nil {
181-
return nil, fmt.Errorf("Error listing ArgoCD applications: %v", err)
201+
return nil, fmt.Errorf("Error listing ArgoCD applications: %w", err)
182202
}
183203
if len(foundApps.Items) == 0 {
184204
return nil, fmt.Errorf("No ArgoCD application found for component path sha1 %s(repo %s), used this label selector: %s", componentPathSha1, repo, labelSelector)
@@ -231,6 +251,54 @@ func findArgocdAppByManifestPathAnnotation(ctx context.Context, componentPath st
231251
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))
232252
}
233253

254+
func SetArgoCDAppRevision(ctx context.Context, componentPath string, revision string, repo string, useSHALabelForArgoDicovery bool) error {
255+
var foundApp *argoappv1.Application
256+
var err error
257+
ac, err := createArgoCdClients()
258+
if err != nil {
259+
return fmt.Errorf("Error creating ArgoCD clients: %w", err)
260+
}
261+
if useSHALabelForArgoDicovery {
262+
foundApp, err = findArgocdAppBySHA1Label(ctx, componentPath, repo, ac.app)
263+
} else {
264+
foundApp, err = findArgocdAppByManifestPathAnnotation(ctx, componentPath, repo, ac.app)
265+
}
266+
if err != nil {
267+
return fmt.Errorf("error finding ArgoCD application for component path %s: %w", componentPath, err)
268+
}
269+
if foundApp.Spec.Source.TargetRevision == revision {
270+
log.Infof("App %s already has revision %s", foundApp.Name, revision)
271+
return nil
272+
}
273+
274+
patchObject := struct {
275+
Spec struct {
276+
Source struct {
277+
TargetRevision string `json:"targetRevision"`
278+
} `json:"source"`
279+
} `json:"spec"`
280+
}{}
281+
patchObject.Spec.Source.TargetRevision = revision
282+
patchJson, _ := json.Marshal(patchObject)
283+
patch := string(patchJson)
284+
log.Debugf("Patching app %s/%s with: %s", foundApp.Namespace, foundApp.Name, patch)
285+
286+
patchType := "merge"
287+
_, err = ac.app.Patch(ctx, &application.ApplicationPatchRequest{
288+
Name: &foundApp.Name,
289+
AppNamespace: &foundApp.Namespace,
290+
PatchType: &patchType,
291+
Patch: &patch,
292+
})
293+
if err != nil {
294+
return fmt.Errorf("revision patching failed: %w", err)
295+
} else {
296+
log.Infof("ArgoCD App %s revision set to %s", foundApp.Name, revision)
297+
}
298+
299+
return err
300+
}
301+
234302
func generateDiffOfAComponent(ctx context.Context, componentPath string, prBranch string, repo string, appClient application.ApplicationServiceClient, projClient projectpkg.ProjectServiceClient, argoSettings *settings.Settings, useSHALabelForArgoDicovery bool) (componentDiffResult DiffResult) {
235303
componentDiffResult.ComponentPath = componentPath
236304

@@ -266,6 +334,12 @@ func generateDiffOfAComponent(ctx context.Context, componentPath string, prBranc
266334
log.Debugf("Got ArgoCD app %s", app.Name)
267335
componentDiffResult.ArgoCdAppName = app.Name
268336
componentDiffResult.ArgoCdAppURL = fmt.Sprintf("%s/applications/%s", argoSettings.URL, app.Name)
337+
338+
if app.Spec.Source.TargetRevision == prBranch {
339+
componentDiffResult.DiffError = fmt.Errorf("App %s already has revision %s as Source Target Revision, skipping diff calculation", app.Name, prBranch)
340+
return componentDiffResult
341+
}
342+
269343
resources, err := appClient.ManagedResources(ctx, &application.ResourcesQuery{ApplicationName: &app.Name, AppNamespace: &app.Namespace})
270344
if err != nil {
271345
componentDiffResult.DiffError = err
@@ -313,41 +387,21 @@ func GenerateDiffOfChangedComponents(ctx context.Context, componentPathList []st
313387
hasComponentDiff = false
314388
hasComponentDiffErrors = false
315389
// env var should be centralized
316-
client, err := createArgoCdClient()
390+
ac, err := createArgoCdClients()
317391
if err != nil {
318-
log.Errorf("Error creating ArgoCD client: %v", err)
392+
log.Errorf("Error creating ArgoCD clients: %v", err)
319393
return false, true, nil, err
320394
}
321395

322-
conn, appClient, err := client.NewApplicationClient()
323-
if err != nil {
324-
log.Errorf("Error creating ArgoCD app client: %v", err)
325-
return false, true, nil, err
326-
}
327-
defer argoio.Close(conn)
328-
329-
conn, projClient, err := client.NewProjectClient()
330-
if err != nil {
331-
log.Errorf("Error creating ArgoCD project client: %v", err)
332-
return false, true, nil, err
333-
}
334-
defer argoio.Close(conn)
335-
336-
conn, settingClient, err := client.NewSettingsClient()
337-
if err != nil {
338-
log.Errorf("Error creating ArgoCD settings client: %v", err)
339-
return false, true, nil, err
340-
}
341-
defer argoio.Close(conn)
342-
argoSettings, err := settingClient.Get(ctx, &settings.SettingsQuery{})
396+
argoSettings, err := ac.setting.Get(ctx, &settings.SettingsQuery{})
343397
if err != nil {
344398
log.Errorf("Error getting ArgoCD settings: %v", err)
345399
return false, true, nil, err
346400
}
347401

348402
log.Debugf("Checking ArgoCD diff for components: %v", componentPathList)
349403
for _, componentPath := range componentPathList {
350-
currentDiffResult := generateDiffOfAComponent(ctx, componentPath, prBranch, repo, appClient, projClient, argoSettings, useSHALabelForArgoDicovery)
404+
currentDiffResult := generateDiffOfAComponent(ctx, componentPath, prBranch, repo, ac.app, ac.project, argoSettings, useSHALabelForArgoDicovery)
351405
if currentDiffResult.DiffError != nil {
352406
log.Errorf("Error generating diff for component %s: %v", componentPath, currentDiffResult.DiffError)
353407
hasComponentDiffErrors = true

internal/pkg/configuration/config.go

+10-9
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,16 @@ type Config struct {
3636
PromotionPaths []PromotionPath `yaml:"promotionPaths"`
3737

3838
// Generic configuration
39-
PromtionPrLables []string `yaml:"promtionPRlables"`
40-
DryRunMode bool `yaml:"dryRunMode"`
41-
AutoApprovePromotionPrs bool `yaml:"autoApprovePromotionPrs"`
42-
ToggleCommitStatus map[string]string `yaml:"toggleCommitStatus"`
43-
WebhookEndpointRegexs []WebhookEndpointRegex `yaml:"webhookEndpointRegexs"`
44-
WhProxtSkipTLSVerifyUpstream bool `yaml:"whProxtSkipTLSVerifyUpstream"`
45-
CommentArgocdDiffonPR bool `yaml:"commentArgocdDiffonPR"`
46-
AutoMergeNoDiffPRs bool `yaml:"autoMergeNoDiffPRs"`
47-
UseSHALabelForArgoDicovery bool `yaml:"useSHALabelForArgoDicovery"`
39+
PromtionPrLables []string `yaml:"promtionPRlables"`
40+
DryRunMode bool `yaml:"dryRunMode"`
41+
AutoApprovePromotionPrs bool `yaml:"autoApprovePromotionPrs"`
42+
ToggleCommitStatus map[string]string `yaml:"toggleCommitStatus"`
43+
WebhookEndpointRegexs []WebhookEndpointRegex `yaml:"webhookEndpointRegexs"`
44+
WhProxtSkipTLSVerifyUpstream bool `yaml:"whProxtSkipTLSVerifyUpstream"`
45+
CommentArgocdDiffonPR bool `yaml:"commentArgocdDiffonPR"`
46+
AutoMergeNoDiffPRs bool `yaml:"autoMergeNoDiffPRs"`
47+
AllowSyncArgoCDAppfromBranchPathRegex string `yaml:"allowSyncArgoCDAppfromBranchPathRegex"`
48+
UseSHALabelForArgoDicovery bool `yaml:"useSHALabelForArgoDicovery"`
4849
}
4950

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

0 commit comments

Comments
 (0)