From aa5f386667f7cf30fb507599dcb75b7b8fbfa7b1 Mon Sep 17 00:00:00 2001 From: Oded Ben Ozer Date: Wed, 19 Jun 2024 15:05:07 +0200 Subject: [PATCH] Sync PR 2 (#14) Sync Changes from upstream * Document APPROVER_GITHUB_APP_ID and APPROVER_GITHUB_APP_PRIVATE_KEY_PATH env vars (#189) * Add a Target Description configuration keys to provide control over promotion PR titles (#191) * Allow skipping upstream TLS server certificate validation for the webhook proxy functionality (#190) --- docs/installation.md | 15 ++- go.mod | 3 +- go.sum | 8 ++ internal/pkg/configuration/config.go | 21 ++-- internal/pkg/githubapi/github.go | 2 +- internal/pkg/githubapi/promotion.go | 6 + internal/pkg/githubapi/promotion_test.go | 151 +++++++++++++++++++++++ internal/pkg/githubapi/webhook_proxy.go | 13 +- 8 files changed, 202 insertions(+), 17 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 0ccdc27d..cb727a45 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -75,6 +75,10 @@ Environment variables for the webhook process: `APPROVER_GITHUB_OAUTH_TOKEN` GitHub OAuth token for automatically approving promotion PRs +`APPROVER_GITHUB_APP_ID` is an alternative to `APPROVER_GITHUB_OAUTH_TOKEN`. You can also use GitHub App style of authentication for the automated PR approval process. This variable supplies the Application ID. + +`APPROVER_GITHUB_APP_PRIVATE_KEY_PATH` is an alternative to `APPROVER_GITHUB_OAUTH_TOKEN`. You can also use GitHub App style of authentication for the automated PR approval process. This variable supplies the path to the Github Application private key file (in `.pem` format). + `GITHUB_OAUTH_TOKEN` GitHub main OAuth token for all other GH operations `GITHUB_HOST` Host name for github API, needed for Github Enterprise Server, should not include http scheme and path, e.g. :`my-gh-host.com` @@ -114,9 +118,11 @@ Configuration keys: |`promotionPaths[0].conditions.autoMerge`| Boolean value. If set to true, PR will be automatically merged after it is created.| |`promotionPaths[0].promotionPrs`| Array of structures, each element represent a PR that will be opened when files are changed under `sourcePath`. Multiple elements means multiple PR will be opened| |`promotionPaths[0].promotionPrs[0].targetPaths`| Array of strings, each element represent a directory to by synced from the changed component under `sourcePath`. Multiple elements means multiple directories will be synced in a PR| +|`promotionPaths[0].promotionPrs[0].targetDescription`| An optional string that describes the target paths, will be used in the promotion PR titles, for example "All Staging Clusters" or "Production Tier 2 Clusters". If this value is not provided Telefonistka will concatenate all `targetPaths` in the PR title which can make it very long and unreadable. Regardless of this configuration key, the PR titles will always start with the component name, e.g. `🚀 Promotion: nginx ➡️ Production Tier 2 Clusters` | |`dryRunMode`| if true, the bot will just comment the planned promotion on the merged PR| |`autoApprovePromotionPrs`| if true the bot will auto-approve all promotion PRs, with the assumption the original PR was peer reviewed and is promoted verbatim. Required additional GH token via APPROVER_GITHUB_OAUTH_TOKEN env variable| |`toggleCommitStatus`| Map of strings, allow (non-repo-admin) users to change the [Github commit status](https://docs.github.com/en/rest/commits/statuses) state(from failure to success and back). This can be used to continue promotion of a change that doesn't pass repo checks. the keys are strings commented in the PRs, values are [Github commit status context](https://docs.github.com/en/rest/commits/statuses?apiVersion=2022-11-28#create-a-commit-status) to be overridden| +|`whProxtSkipTLSVerifyUpstream`| This disables upstream TLS server certificate validation for the webhook proxy functionality. Default is `false`. | |`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. | |`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)| |`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`.| @@ -130,7 +136,8 @@ promotionPaths: conditions: autoMerge: true promotionPrs: - - targetPaths: + - targetDescription: "All non-production clusters" + targetPaths: - "clusters/dev/us-east4/c2" - "clusters/lab/europe-west4/c1" - "clusters/staging/us-central1/c1" @@ -141,9 +148,11 @@ promotionPaths: prHasLabels: - "quick_promotion" # This flow will run only if PR has "quick_promotion" label, see targetPaths below promotionPrs: - - targetPaths: + - targetDescription: "Production clusters tier 1" + targetPaths: - "clusters/prod/us-west1/c2" # First PR for only a single cluster - - targetPaths: + - targetDescription: "Production clusters tier 2" + targetPaths: - "clusters/prod/europe-west3/c2" # 2nd PR will sync all 4 remaining clusters - "clusters/prod/europe-west4/c2" - "clusters/prod/us-central1/c2" diff --git a/go.mod b/go.mod index ac697386..75a9e971 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,9 @@ go 1.22 toolchain go1.22.1 +require github.com/alexliesenfeld/health v0.8.0 + require ( - github.com/alexliesenfeld/health v0.8.0 github.com/argoproj/argo-cd/v2 v2.11.2 github.com/argoproj/gitops-engine v0.7.1-0.20240416142647-fbecbb86e412 github.com/bradleyfalzon/ghinstallation/v2 v2.10.0 diff --git a/go.sum b/go.sum index fd39ba5e..2b2ee61f 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ + cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -638,6 +639,7 @@ github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alexliesenfeld/health v0.8.0 h1:lCV0i+ZJPTbqP7LfKG7p3qZBl5VhelwUFCIVWl77fgk= github.com/alexliesenfeld/health v0.8.0/go.mod h1:TfNP0f+9WQVWMQRzvMUjlws4ceXKEL3WR+6Hp95HUFc= + github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/miniredis/v2 v2.30.4 h1:8S4/o1/KoUArAGbGwPxcwf0krlzceva2XVOSchFS7Eo= @@ -835,6 +837,7 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= + github.com/go-redis/cache/v9 v9.0.0 h1:0thdtFo0xJi0/WXbRVu8B066z8OvVymXTJGaXrVWnN0= github.com/go-redis/cache/v9 v9.0.0/go.mod h1:cMwi1N8ASBOufbIvk7cdXe2PbPjK/WMRL95FFHWsSgI= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -850,6 +853,7 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-yaml v1.11.3 h1:B3W9IdWbvrUu2OYQGwvU1nZtvMQJPBKgBUuweJjLj6I= github.com/goccy/go-yaml v1.11.3/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= + github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -932,6 +936,7 @@ github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwM github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= + github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -1086,6 +1091,7 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= + github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/migueleliasweb/go-github-mock v0.0.22 h1:iUvUKmYd7sFq/wrb9TrbEdvc30NaYxLZNtz7Uv2D+AQ= github.com/migueleliasweb/go-github-mock v0.0.22/go.mod h1:UVvZ3S9IdTTRqThr1lgagVaua3Jl1bmY4E+C/Vybbn4= @@ -1236,6 +1242,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= + github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -2023,6 +2030,7 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= + honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/pkg/configuration/config.go b/internal/pkg/configuration/config.go index 379d9c8c..731e033e 100644 --- a/internal/pkg/configuration/config.go +++ b/internal/pkg/configuration/config.go @@ -20,7 +20,8 @@ type Condition struct { } type PromotionPr struct { - TargetPaths []string `yaml:"targetPaths"` + TargetDescription string `yaml:"targetDescription"` + TargetPaths []string `yaml:"targetPaths"` } type PromotionPath struct { @@ -35,14 +36,16 @@ type Config struct { PromotionPaths []PromotionPath `yaml:"promotionPaths"` // Generic configuration - PromtionPrLables []string `yaml:"promtionPRlables"` - DryRunMode bool `yaml:"dryRunMode"` - AutoApprovePromotionPrs bool `yaml:"autoApprovePromotionPrs"` - ToggleCommitStatus map[string]string `yaml:"toggleCommitStatus"` - WebhookEndpointRegexs []WebhookEndpointRegex `yaml:"webhookEndpointRegexs"` - CommentArgocdDiffonPR bool `yaml:"commentArgocdDiffonPR"` - AutoMergeNoDiffPRs bool `yaml:"autoMergeNoDiffPRs"` - UseSHALabelForArgoDicovery bool `yaml:"useSHALabelForArgoDicovery"` + + PromtionPrLables []string `yaml:"promtionPRlables"` + DryRunMode bool `yaml:"dryRunMode"` + AutoApprovePromotionPrs bool `yaml:"autoApprovePromotionPrs"` + ToggleCommitStatus map[string]string `yaml:"toggleCommitStatus"` + WebhookEndpointRegexs []WebhookEndpointRegex `yaml:"webhookEndpointRegexs"` + WhProxtSkipTLSVerifyUpstream bool `yaml:"whProxtSkipTLSVerifyUpstream"` + CommentArgocdDiffonPR bool `yaml:"commentArgocdDiffonPR"` + AutoMergeNoDiffPRs bool `yaml:"autoMergeNoDiffPRs"` + UseSHALabelForArgoDicovery bool `yaml:"useSHALabelForArgoDicovery"` } func ParseConfigFromYaml(y string) (*Config, error) { diff --git a/internal/pkg/githubapi/github.go b/internal/pkg/githubapi/github.go index a7e3cce7..e5136b61 100644 --- a/internal/pkg/githubapi/github.go +++ b/internal/pkg/githubapi/github.go @@ -425,7 +425,7 @@ func handleMergedPrEvent(ghPrClientDetails GhPrClientDetails, prApproverGithubCl } components := strings.Join(promotion.Metadata.ComponentNames, ",") - newPrTitle := fmt.Sprintf("🚀 Promotion: %s ➡️ %s", components, strings.Join(promotion.Metadata.TargetPaths, " ")) + newPrTitle := fmt.Sprintf("🚀 Promotion: %s ➡️ %s", components, promotion.Metadata.TargetDescription) var originalPrAuthor string // If the triggering PR was opened manually and it doesn't include in-body metadata, use the PR author diff --git a/internal/pkg/githubapi/promotion.go b/internal/pkg/githubapi/promotion.go index 22558916..2b8daa45 100644 --- a/internal/pkg/githubapi/promotion.go +++ b/internal/pkg/githubapi/promotion.go @@ -21,6 +21,7 @@ type PromotionInstance struct { type PromotionInstanceMetaData struct { SourcePath string TargetPaths []string + TargetDescription string PerComponentSkippedTargetPaths map[string][]string // ComponentName is the key, ComponentNames []string AutoMerge bool @@ -121,6 +122,7 @@ func generateListOfRelevantComponents(ghPrClientDetails GhPrClientDetails, confi prom.InstrumentGhCall(resp) if err != nil { ghPrClientDetails.PrLogger.Errorf("could not get file list from GH API: err=%s\nstatus code=%v", err, resp.Response.Status) + return nil, err } prFiles = append(prFiles, perPagePrFiles...) @@ -208,9 +210,13 @@ func generatePlanBasedOnChangeddComponent(ghPrClientDetails GhPrClientDetails, c mapKey := configPromotionPath.SourcePath + ">" + strings.Join(ppr.TargetPaths, "|") // This key is used to aggregate the PR based on source and target combination if entry, ok := promotions[mapKey]; !ok { ghPrClientDetails.PrLogger.Debugf("Adding key %s", mapKey) + if ppr.TargetDescription == "" { + ppr.TargetDescription = strings.Join(ppr.TargetPaths, " ") + } promotions[mapKey] = PromotionInstance{ Metadata: PromotionInstanceMetaData{ TargetPaths: ppr.TargetPaths, + TargetDescription: ppr.TargetDescription, SourcePath: componentToPromote.SourcePath, ComponentNames: []string{componentToPromote.ComponentName}, PerComponentSkippedTargetPaths: map[string][]string{}, diff --git a/internal/pkg/githubapi/promotion_test.go b/internal/pkg/githubapi/promotion_test.go index 081ecc8a..0d2b6dfe 100644 --- a/internal/pkg/githubapi/promotion_test.go +++ b/internal/pkg/githubapi/promotion_test.go @@ -12,6 +12,42 @@ import ( cfg "github.com/wayfair-incubator/telefonistka/internal/pkg/configuration" ) +func generatePromotionPlanMetadataTestHelper(t *testing.T, config *cfg.Config, expectedPromotion map[string]PromotionInstance, mockedHTTPClient *http.Client) { + t.Helper() + ctx := context.Background() + ghClientPair := GhClientPair{v3Client: github.NewClient(mockedHTTPClient)} + labelName := "fast-promotion" + + ghPrClientDetails := GhPrClientDetails{ + Ctx: ctx, + GhClientPair: &ghClientPair, + Owner: "AnOwner", + Repo: "Arepo", + PrNumber: 120, + Ref: "Abranch", + PrLogger: log.WithFields(log.Fields{ + "repo": "AnOwner/Arepo", + "prNumber": 120, + }), + Labels: []*github.Label{ + {Name: &labelName}, + }, + } + + promotionPlan, err := GeneratePromotionPlan(ghPrClientDetails, config, "main") + if err != nil { + t.Fatalf("Failed to generate promotion plan: err=%s", err) + } + + // Just check the metadata, this can ignore issues with promotion logic itself + // Like if a whole element is missing from the generated promotion plan. + for k, v := range expectedPromotion { + if diff := deep.Equal(v.Metadata, promotionPlan[k].Metadata); diff != nil { + t.Error(diff) + } + } +} + func generatePromotionPlanTestHelper(t *testing.T, config *cfg.Config, expectedPromotion map[string]PromotionInstance, mockedHTTPClient *http.Client) { t.Helper() ctx := context.Background() @@ -533,3 +569,118 @@ func TestGeneratePromotionPlanWithPagination(t *testing.T) { ) generatePromotionPlanTestHelper(t, config, expectedPromotion, mockedHTTPClient) } + +// TestGeneratePromotionMetadataWithOutDesc tests the case where the target description is set +func TestGeneratePromotionMetadataWithDesc(t *testing.T) { + t.Parallel() + config := &cfg.Config{ + PromotionPaths: []cfg.PromotionPath{ + { + SourcePath: "prod/us-east-4/", + PromotionPrs: []cfg.PromotionPr{ + { + TargetDescription: "foobar2", // This is tested config key + TargetPaths: []string{ + "prod/eu-west-1/", + "prod/eu-east-1/", + }, + }, + }, + }, + }, + } + expectedPromotion := map[string]PromotionInstance{ + "prod/us-east-4/>prod/eu-east-1/|prod/eu-west-1/": { + ComputedSyncPaths: map[string]string{ + "prod/eu-east-1/componentA": "prod/us-east-4/componentA", + "prod/eu-west-1/componentA": "prod/us-east-4/componentA", + }, + Metadata: PromotionInstanceMetaData{ + SourcePath: "prod/us-east-4/", + TargetDescription: "foobar2", // This is tested config key + TargetPaths: []string{"prod/eu-east-1/", "prod/eu-west-1/"}, + PerComponentSkippedTargetPaths: map[string][]string{}, + ComponentNames: []string{"componentA"}, + }, + }, + } + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetReposPullsFilesByOwnerByRepoByPullNumber, + []github.CommitFile{ + {Filename: github.String("prod/us-east-4/componentA/file.yaml")}, + {Filename: github.String("prod/us-east-4/componentA/file2.yaml")}, + {Filename: github.String("prod/us-east-4/componentA/aSubDir/file3.yaml")}, + {Filename: github.String(".ci-config/random-file.json")}, + }, + ), + mock.WithRequestMatchHandler( + mock.GetReposContentsByOwnerByRepoByPath, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mock.WriteError( + w, + http.StatusNotFound, + "no *optional* in-component telefonistka config file", + ) + }), + ), + ) + generatePromotionPlanMetadataTestHelper(t, config, expectedPromotion, mockedHTTPClient) +} + +// This test is similar to the previous one, but the TargetDescription is not set in the config +func TestGeneratePromotionMetadataWithOutDesc(t *testing.T) { + t.Parallel() + config := &cfg.Config{ + PromotionPaths: []cfg.PromotionPath{ + { + SourcePath: "prod/us-east-4/", + PromotionPrs: []cfg.PromotionPr{ + { + TargetPaths: []string{ + "prod/eu-west-1/", + "prod/eu-east-1/", + }, // TargetDescription is not set in this case + }, + }, + }, + }, + } + expectedPromotion := map[string]PromotionInstance{ + "prod/us-east-4/>prod/eu-east-1/|prod/eu-west-1/": { + ComputedSyncPaths: map[string]string{ + "prod/eu-east-1/componentA": "prod/us-east-4/componentA", + "prod/eu-west-1/componentA": "prod/us-east-4/componentA", + }, + Metadata: PromotionInstanceMetaData{ + SourcePath: "prod/us-east-4/", + TargetDescription: "prod/eu-east-1/ prod/eu-west-1/", // This is tested config key + TargetPaths: []string{"prod/eu-east-1/", "prod/eu-west-1/"}, + PerComponentSkippedTargetPaths: map[string][]string{}, + ComponentNames: []string{"componentA"}, + }, + }, + } + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetReposPullsFilesByOwnerByRepoByPullNumber, + []github.CommitFile{ + {Filename: github.String("prod/us-east-4/componentA/file.yaml")}, + {Filename: github.String("prod/us-east-4/componentA/file2.yaml")}, + {Filename: github.String("prod/us-east-4/componentA/aSubDir/file3.yaml")}, + {Filename: github.String(".ci-config/random-file.json")}, + }, + ), + mock.WithRequestMatchHandler( + mock.GetReposContentsByOwnerByRepoByPath, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mock.WriteError( + w, + http.StatusNotFound, + "no *optional* in-component telefonistka config file", + ) + }), + ), + ) + generatePromotionPlanMetadataTestHelper(t, config, expectedPromotion, mockedHTTPClient) +} diff --git a/internal/pkg/githubapi/webhook_proxy.go b/internal/pkg/githubapi/webhook_proxy.go index 3008e951..c3eadb30 100644 --- a/internal/pkg/githubapi/webhook_proxy.go +++ b/internal/pkg/githubapi/webhook_proxy.go @@ -3,6 +3,7 @@ package githubapi import ( "bytes" "context" + "crypto/tls" "fmt" "io" "net/http" @@ -57,8 +58,14 @@ func generateListOfEndpoints(listOfChangedFiles []string, config *configuration. return maps.Keys(endpoints) } -func proxyRequest(ctx context.Context, originalHttpRequest *http.Request, body []byte, endpoint string, responses chan<- string) { - client := &http.Client{} +func proxyRequest(ctx context.Context, skipTLSVerify bool, originalHttpRequest *http.Request, body []byte, endpoint string, responses chan<- string) { + tr := &http.Transport{} + if skipTLSVerify { + tr = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // #nosec G402 - letting the user decide if they want to skip TLS verification, for some in-cluster scenarios its a reasonable compromise + } + } + client := &http.Client{Transport: tr} req, err := http.NewRequestWithContext(ctx, originalHttpRequest.Method, endpoint, bytes.NewBuffer(body)) if err != nil { log.Errorf("Error creating request to %s: %v", endpoint, err) @@ -117,7 +124,7 @@ func handlePushEvent(ctx context.Context, eventPayload *github.PushEvent, httpRe // Start a goroutine for each endpoint for _, endpoint := range endpoints { - go proxyRequest(ctx, httpRequest, payload, endpoint, responses) + go proxyRequest(ctx, config.WhProxtSkipTLSVerifyUpstream, httpRequest, payload, endpoint, responses) } // Wait for all goroutines to finish and collect the responses