|
7 | 7 | "encoding/json"
|
8 | 8 | "fmt"
|
9 | 9 | "os"
|
| 10 | + "path/filepath" |
10 | 11 | "strconv"
|
| 12 | + "strings" |
11 | 13 |
|
12 | 14 | cmdutil "github.com/argoproj/argo-cd/v2/cmd/util"
|
13 | 15 | "github.com/argoproj/argo-cd/v2/pkg/apiclient"
|
@@ -151,42 +153,102 @@ func createArgoCdClient() (apiclient.Client, error) {
|
151 | 153 | return clientset, nil
|
152 | 154 | }
|
153 | 155 |
|
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) { |
157 | 159 | // Calculate sha1 of component path to use in a label selector
|
158 | 160 | cPathBa := []byte(componentPath)
|
159 | 161 | hasher := sha1.New() //nolint:gosec // G505: Blocklisted import crypto/sha1: weak cryptographic primitive (gosec), this is not a cryptographic use case
|
160 | 162 | hasher.Write(cPathBa)
|
161 | 163 | 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). |
165 | 164 | labelSelector := fmt.Sprintf("telefonistka.io/component-path-sha1=%s", componentPathSha1)
|
166 | 165 | appLabelQuery := application.ApplicationQuery{
|
167 | 166 | Selector: &labelSelector,
|
168 | 167 | Repo: &repo,
|
169 | 168 | }
|
170 | 169 | foundApps, err := appIf.List(ctx, &appLabelQuery)
|
171 | 170 | if err != nil {
|
172 |
| - componentDiffResult.DiffError = err |
173 |
| - return componentDiffResult |
| 171 | + return nil, err |
174 | 172 | }
|
175 | 173 | 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 |
177 | 238 | return componentDiffResult
|
178 | 239 | }
|
179 | 240 |
|
180 | 241 | // 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" |
181 | 243 | refreshType := string(argoappv1.RefreshTypeHard)
|
182 | 244 | 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 |
184 | 246 | Refresh: &refreshType,
|
185 | 247 | }
|
186 | 248 | app, err := appIf.Get(ctx, &appNameQuery)
|
187 | 249 | if err != nil {
|
188 | 250 | 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) |
190 | 252 | return componentDiffResult
|
191 | 253 | }
|
192 | 254 | componentDiffResult.ArgoCdAppName = app.Name
|
@@ -232,7 +294,7 @@ func generateDiffOfAComponent(ctx context.Context, componentPath string, prBranc
|
232 | 294 | }
|
233 | 295 |
|
234 | 296 | // 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) { |
236 | 298 | hasComponentDiff = false
|
237 | 299 | hasComponentDiffErrors = false
|
238 | 300 | // env var should be centralized
|
@@ -264,7 +326,7 @@ func GenerateDiffOfChangedComponents(ctx context.Context, componentPathList []st
|
264 | 326 | }
|
265 | 327 |
|
266 | 328 | 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) |
268 | 330 | if currentDiffResult.DiffError != nil {
|
269 | 331 | hasComponentDiffErrors = true
|
270 | 332 | err = currentDiffResult.DiffError
|
|
0 commit comments