Skip to content

Commit e2b552d

Browse files
authored
Handle/mitigate crash and context issues (#11)
* Use new context (not a child) * Remove calls to a function that exits the process. * Add more "context"(info, like for humans) to errors * Address another err with no context issue * Respond the webhook only based on payload parsing. Actual Event processing is move to background thread. This require some refactoring, created ReciveWebhook and ReciveEventFile functions to represent the different behavior in Web Server VS CLI triggering while keeping to the GH stuff in the GH package * Cancel whole drift work on context deadline * Move error function return value to the standard position Handle cases where GetContents returns nil HTTP response (like in Context cancellation)
1 parent 2549abd commit e2b552d

9 files changed

+94
-57
lines changed

cmd/telefonistka/bump-version-overwrite.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func bumpVersionOverwrite(targetRepo string, targetFile string, file string, git
7373
ghPrClientDetails.PrLogger = log.WithFields(log.Fields{}) // TODO what fields should be here?
7474

7575
defaultBranch, _ := ghPrClientDetails.GetDefaultBranch()
76-
initialFileContent, err, statusCode := githubapi.GetFileContent(ghPrClientDetails, defaultBranch, targetFile)
76+
initialFileContent, statusCode, err := githubapi.GetFileContent(ghPrClientDetails, defaultBranch, targetFile)
7777
if statusCode == 404 {
7878
ghPrClientDetails.PrLogger.Infof("File %s was not found\n", targetFile)
7979
} else if err != nil {

cmd/telefonistka/bump-version-regex.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func bumpVersionRegex(targetRepo string, targetFile string, regex string, replac
7171
r := regexp.MustCompile(regex)
7272
defaultBranch, _ := ghPrClientDetails.GetDefaultBranch()
7373

74-
initialFileContent, err, _ := githubapi.GetFileContent(ghPrClientDetails, defaultBranch, targetFile)
74+
initialFileContent, _, err := githubapi.GetFileContent(ghPrClientDetails, defaultBranch, targetFile)
7575
if err != nil {
7676
ghPrClientDetails.PrLogger.Errorf("Fail to fetch file content:%s\n", err)
7777
os.Exit(1)

cmd/telefonistka/bump-version-yaml.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ func bumpVersionYaml(targetRepo string, targetFile string, address string, value
7474

7575
defaultBranch, _ := ghPrClientDetails.GetDefaultBranch()
7676

77-
initialFileContent, err, _ := githubapi.GetFileContent(ghPrClientDetails, defaultBranch, targetFile)
77+
initialFileContent, _, err := githubapi.GetFileContent(ghPrClientDetails, defaultBranch, targetFile)
7878
if err != nil {
7979
ghPrClientDetails.PrLogger.Errorf("Fail to fetch file content:%s\n", err)
8080
os.Exit(1)

cmd/telefonistka/event.go

+1-24
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
package telefonistka
22

33
import (
4-
"bytes"
5-
"context"
6-
"io"
7-
"net/http"
84
"os"
95

106
lru "github.com/hashicorp/golang-lru/v2"
11-
log "github.com/sirupsen/logrus"
127
"github.com/spf13/cobra"
138
"github.com/wayfair-incubator/telefonistka/internal/pkg/githubapi"
149
)
@@ -32,27 +27,9 @@ func init() { //nolint:gochecknoinits
3227
}
3328

3429
func event(eventType string, eventFilePath string) {
35-
ctx := context.Background()
36-
37-
log.Infof("Event type: %s", eventType)
38-
log.Infof("Proccesing file: %s", eventFilePath)
39-
40-
payload, err := os.ReadFile(eventFilePath)
41-
if err != nil {
42-
panic(err)
43-
}
44-
45-
// To use the same code path as for Webhook I'm creating an http request with the payload from the file.
46-
// This might not be very smart.
47-
48-
h, _ := http.NewRequest("POST", "", nil) //nolint:noctx
49-
h.Body = io.NopCloser(bytes.NewReader(payload))
50-
h.Header.Set("Content-Type", "application/json")
51-
h.Header.Set("X-GitHub-Event", eventType)
52-
5330
mainGhClientCache, _ := lru.New[string, githubapi.GhClientPair](128)
5431
prApproverGhClientCache, _ := lru.New[string, githubapi.GhClientPair](128)
55-
githubapi.HandleEvent(h, ctx, mainGhClientCache, prApproverGhClientCache, nil)
32+
githubapi.ReciveEventFile(eventFilePath, eventType, mainGhClientCache, prApproverGhClientCache)
5633
}
5734

5835
func getEnv(key, fallback string) string {

cmd/telefonistka/server.go

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package telefonistka
22

33
import (
4-
"context"
54
"net/http"
65
"os"
76
"time"
@@ -39,9 +38,13 @@ func init() { //nolint:gochecknoinits
3938

4039
func handleWebhook(githubWebhookSecret []byte, mainGhClientCache *lru.Cache[string, githubapi.GhClientPair], prApproverGhClientCache *lru.Cache[string, githubapi.GhClientPair]) func(http.ResponseWriter, *http.Request) {
4140
return func(w http.ResponseWriter, r *http.Request) {
42-
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
43-
defer cancel()
44-
githubapi.HandleEvent(r, ctx, mainGhClientCache, prApproverGhClientCache, githubWebhookSecret)
41+
err := githubapi.ReciveWebhook(r, mainGhClientCache, prApproverGhClientCache, githubWebhookSecret)
42+
if err != nil {
43+
log.Errorf("error handling webhook: %v", err)
44+
http.Error(w, "Internal server error", http.StatusInternalServerError)
45+
return
46+
}
47+
w.WriteHeader(http.StatusOK)
4548
}
4649
}
4750

internal/pkg/argocd/argocd.go

+15-9
Original file line numberDiff line numberDiff line change
@@ -51,20 +51,26 @@ type DiffResult struct {
5151
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) {
5252
liveObjs, err := cmdutil.LiveObjects(resources.Items)
5353
if err != nil {
54-
return false, nil, err
54+
return false, nil, fmt.Errorf("Failed to get live objects: %v", err)
5555
}
5656

5757
items := make([]objKeyLiveTarget, 0)
5858
var unstructureds []*unstructured.Unstructured
5959
for _, mfst := range diffOptions.res.Manifests {
6060
obj, err := argoappv1.UnmarshalToUnstructured(mfst)
6161
if err != nil {
62-
return false, nil, err
62+
return false, nil, fmt.Errorf("Failed to unmarshal manifest: %v", err)
6363
}
6464
unstructureds = append(unstructureds, obj)
6565
}
66-
groupedObjs := groupObjsByKey(unstructureds, liveObjs, app.Spec.Destination.Namespace)
67-
items = groupObjsForDiff(resources, groupedObjs, items, argoSettings, app.InstanceName(argoSettings.ControllerNamespace), app.Spec.Destination.Namespace)
66+
groupedObjs, err := groupObjsByKey(unstructureds, liveObjs, app.Spec.Destination.Namespace)
67+
if err != nil {
68+
return false, nil, fmt.Errorf("Failed to group objects by key: %v", err)
69+
}
70+
items, err = groupObjsForDiff(resources, groupedObjs, items, argoSettings, app.InstanceName(argoSettings.ControllerNamespace), app.Spec.Destination.Namespace)
71+
if err != nil {
72+
return false, nil, fmt.Errorf("Failed to group objects for diff: %v", err)
73+
}
6874

6975
for _, item := range items {
7076
var diffElement DiffElement
@@ -85,11 +91,11 @@ func generateArgocdAppDiff(ctx context.Context, app *argoappv1.Application, proj
8591
WithNoCache().
8692
Build()
8793
if err != nil {
88-
return false, nil, err
94+
return false, nil, fmt.Errorf("Failed to build diff config: %v", err)
8995
}
9096
diffRes, err := argodiff.StateDiff(item.live, item.target, diffConfig)
9197
if err != nil {
92-
return false, nil, err
98+
return false, nil, fmt.Errorf("Failed to diff objects: %v", err)
9399
}
94100

95101
if diffRes.Modified || item.target == nil || item.live == nil {
@@ -105,7 +111,7 @@ func generateArgocdAppDiff(ctx context.Context, app *argoappv1.Application, proj
105111
live = item.live
106112
err = json.Unmarshal(diffRes.PredictedLive, target)
107113
if err != nil {
108-
return false, nil, err
114+
return false, nil, fmt.Errorf("Failed to unmarshal predicted live object: %v", err)
109115
}
110116
} else {
111117
live = item.live
@@ -117,7 +123,7 @@ func generateArgocdAppDiff(ctx context.Context, app *argoappv1.Application, proj
117123

118124
diffElement.Diff, err = diffLiveVsTargetObject(live, target)
119125
if err != nil {
120-
return false, nil, err
126+
return false, nil, fmt.Errorf("Failed to diff live objects: %v", err)
121127
}
122128
}
123129
diffElements = append(diffElements, diffElement)
@@ -151,7 +157,7 @@ func createArgoCdClient() (apiclient.Client, error) {
151157

152158
clientset, err := apiclient.NewClient(opts)
153159
if err != nil {
154-
return nil, err
160+
return nil, fmt.Errorf("Error creating ArgoCD API client: %v", err)
155161
}
156162
return clientset, nil
157163
}

internal/pkg/argocd/argocd_copied_from_upstream.go

+14-8
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ package argocd
22

33
import (
44
"encoding/json"
5+
"fmt"
56

67
"github.com/argoproj/argo-cd/v2/controller"
78
"github.com/argoproj/argo-cd/v2/pkg/apiclient/application"
89
"github.com/argoproj/argo-cd/v2/pkg/apiclient/settings"
910
argoappv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
1011
repoapiclient "github.com/argoproj/argo-cd/v2/reposerver/apiclient"
1112
"github.com/argoproj/argo-cd/v2/util/argo"
12-
"github.com/argoproj/argo-cd/v2/util/errors"
1313
"github.com/argoproj/gitops-engine/pkg/sync/hook"
1414
"github.com/argoproj/gitops-engine/pkg/sync/ignore"
1515
"github.com/argoproj/gitops-engine/pkg/utils/kube"
@@ -42,7 +42,7 @@ func (p *resourceInfoProvider) IsNamespaced(gk schema.GroupKind) (bool, error) {
4242
// This function creates a map of objects by key(object name/kind/ns) from the rendered manifests.
4343
// That map is used to compare the objects in the application with the objects in the cluster.
4444
// copied from https://github.com/argoproj/argo-cd/blob/4f6a8dce80f0accef7ed3b5510e178a6b398b331/cmd/argocd/commands/app.go#L1091-L1109
45-
func groupObjsByKey(localObs []*unstructured.Unstructured, liveObjs []*unstructured.Unstructured, appNamespace string) map[kube.ResourceKey]*unstructured.Unstructured {
45+
func groupObjsByKey(localObs []*unstructured.Unstructured, liveObjs []*unstructured.Unstructured, appNamespace string) (map[kube.ResourceKey]*unstructured.Unstructured, error) {
4646
namespacedByGk := make(map[schema.GroupKind]bool)
4747
for i := range liveObjs {
4848
if liveObjs[i] != nil {
@@ -51,25 +51,29 @@ func groupObjsByKey(localObs []*unstructured.Unstructured, liveObjs []*unstructu
5151
}
5252
}
5353
localObs, _, err := controller.DeduplicateTargetObjects(appNamespace, localObs, &resourceInfoProvider{namespacedByGk: namespacedByGk})
54-
errors.CheckError(err)
54+
if err != nil {
55+
return nil, fmt.Errorf("Failed to DeDuplicate target objects: %v", err)
56+
}
5557
objByKey := make(map[kube.ResourceKey]*unstructured.Unstructured)
5658
for i := range localObs {
5759
obj := localObs[i]
5860
if !(hook.IsHook(obj) || ignore.Ignore(obj)) {
5961
objByKey[kube.GetResourceKey(obj)] = obj
6062
}
6163
}
62-
return objByKey
64+
return objByKey, nil
6365
}
6466

6567
// This function create a slice of objects to be "diff'ed", each element contains the key, live(in-cluster API state) and target(rended manifest from git) object.
6668
// Copied from https://github.com/argoproj/argo-cd/blob/4f6a8dce80f0accef7ed3b5510e178a6b398b331/cmd/argocd/commands/app.go#L1341-L1372
67-
func groupObjsForDiff(resources *application.ManagedResourcesResponse, objs map[kube.ResourceKey]*unstructured.Unstructured, items []objKeyLiveTarget, argoSettings *settings.Settings, appName, namespace string) []objKeyLiveTarget {
69+
func groupObjsForDiff(resources *application.ManagedResourcesResponse, objs map[kube.ResourceKey]*unstructured.Unstructured, items []objKeyLiveTarget, argoSettings *settings.Settings, appName, namespace string) ([]objKeyLiveTarget, error) {
6870
resourceTracking := argo.NewResourceTracking()
6971
for _, res := range resources.Items {
7072
live := &unstructured.Unstructured{}
7173
err := json.Unmarshal([]byte(res.NormalizedLiveState), &live)
72-
errors.CheckError(err)
74+
if err != nil {
75+
return nil, fmt.Errorf("Failed to unmarshal live object(%v): %v", res.Name, err)
76+
}
7377

7478
key := kube.ResourceKey{Name: res.Name, Namespace: res.Namespace, Group: res.Group, Kind: res.Kind}
7579
if key.Kind == kube.SecretKind && key.Group == "" {
@@ -80,7 +84,9 @@ func groupObjsForDiff(resources *application.ManagedResourcesResponse, objs map[
8084
if local, ok := objs[key]; ok || live != nil {
8185
if local != nil && !kube.IsCRD(local) {
8286
err = resourceTracking.SetAppInstance(local, argoSettings.AppLabelKey, appName, namespace, argoappv1.TrackingMethod(argoSettings.GetTrackingMethod()))
83-
errors.CheckError(err)
87+
if err != nil {
88+
return nil, fmt.Errorf("Failed to set app instance label: %v", err)
89+
}
8490
}
8591

8692
items = append(items, objKeyLiveTarget{key, live, local})
@@ -95,5 +101,5 @@ func groupObjsForDiff(resources *application.ManagedResourcesResponse, objs map[
95101
}
96102
items = append(items, objKeyLiveTarget{key, nil, local})
97103
}
98-
return items
104+
return items, nil
99105
}

internal/pkg/githubapi/github.go

+51-9
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ import (
88
"encoding/hex"
99
"encoding/json"
1010
"fmt"
11+
"io"
1112
"net/http"
13+
"os"
1214
"path"
1315
"regexp"
1416
"sort"
1517
"strings"
1618
"text/template"
19+
"time"
1720

1821
"github.com/cenkalti/backoff/v4"
1922
"github.com/google/go-github/v62/github"
@@ -185,22 +188,56 @@ func HandlePREvent(eventPayload *github.PullRequestEvent, ghPrClientDetails GhPr
185188
}
186189
}
187190

188-
func HandleEvent(r *http.Request, ctx context.Context, mainGhClientCache *lru.Cache[string, GhClientPair], prApproverGhClientCache *lru.Cache[string, GhClientPair], githubWebhookSecret []byte) {
191+
// ReciveEventFile this one is similar to ReciveWebhook but it's used for CLI triggering, i simulates a webhook event to use the same code path as the webhook handler.
192+
func ReciveEventFile(eventType string, eventFilePath string, mainGhClientCache *lru.Cache[string, GhClientPair], prApproverGhClientCache *lru.Cache[string, GhClientPair]) {
193+
log.Infof("Event type: %s", eventType)
194+
log.Infof("Proccesing file: %s", eventFilePath)
195+
196+
payload, err := os.ReadFile(eventFilePath)
197+
if err != nil {
198+
panic(err)
199+
}
200+
eventPayloadInterface, err := github.ParseWebHook(eventType, payload)
201+
if err != nil {
202+
log.Errorf("could not parse webhook: err=%s\n", err)
203+
prom.InstrumentWebhookHit("parsing_failed")
204+
return
205+
}
206+
r, _ := http.NewRequest("POST", "", nil) //nolint:noctx
207+
r.Body = io.NopCloser(bytes.NewReader(payload))
208+
r.Header.Set("Content-Type", "application/json")
209+
r.Header.Set("X-GitHub-Event", eventType)
210+
211+
handleEvent(eventPayloadInterface, mainGhClientCache, prApproverGhClientCache, r, payload, eventType)
212+
}
213+
214+
// ReciveWebhook is the main entry point for the webhook handling it starts parases the webhook payload and start a thread to handle the event success/failure are dependant on the payload parsing only
215+
func ReciveWebhook(r *http.Request, mainGhClientCache *lru.Cache[string, GhClientPair], prApproverGhClientCache *lru.Cache[string, GhClientPair], githubWebhookSecret []byte) error {
189216
payload, err := github.ValidatePayload(r, githubWebhookSecret)
190217
if err != nil {
191218
log.Errorf("error reading request body: err=%s\n", err)
192219
prom.InstrumentWebhookHit("validation_failed")
193-
return
220+
return err
194221
}
195222
eventType := github.WebHookType(r)
196223

197224
eventPayloadInterface, err := github.ParseWebHook(eventType, payload)
198225
if err != nil {
199226
log.Errorf("could not parse webhook: err=%s\n", err)
200227
prom.InstrumentWebhookHit("parsing_failed")
201-
return
228+
return err
202229
}
203230
prom.InstrumentWebhookHit("successful")
231+
232+
go handleEvent(eventPayloadInterface, mainGhClientCache, prApproverGhClientCache, r, payload, eventType)
233+
return nil
234+
}
235+
236+
func handleEvent(eventPayloadInterface interface{}, mainGhClientCache *lru.Cache[string, GhClientPair], prApproverGhClientCache *lru.Cache[string, GhClientPair], r *http.Request, payload []byte, eventType string) {
237+
// We don't use the request context as it might have a short deadline and we don't want to stop event handling based on that
238+
// But we do want to stop the event handling after a certain point, so:
239+
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
240+
defer cancel()
204241
var mainGithubClientPair GhClientPair
205242
var approverGithubClientPair GhClientPair
206243

@@ -971,7 +1008,7 @@ func ApprovePr(approverClient *github.Client, ghPrClientDetails GhPrClientDetail
9711008
}
9721009

9731010
func GetInRepoConfig(ghPrClientDetails GhPrClientDetails, defaultBranch string) (*cfg.Config, error) {
974-
inRepoConfigFileContentString, err, _ := GetFileContent(ghPrClientDetails, defaultBranch, "telefonistka.yaml")
1011+
inRepoConfigFileContentString, _, err := GetFileContent(ghPrClientDetails, defaultBranch, "telefonistka.yaml")
9751012
if err != nil {
9761013
ghPrClientDetails.PrLogger.Errorf("Could not get in-repo configuration: err=%s\n", err)
9771014
return nil, err
@@ -983,18 +1020,23 @@ func GetInRepoConfig(ghPrClientDetails GhPrClientDetails, defaultBranch string)
9831020
return c, err
9841021
}
9851022

986-
func GetFileContent(ghPrClientDetails GhPrClientDetails, branch string, filePath string) (string, error, int) {
1023+
func GetFileContent(ghPrClientDetails GhPrClientDetails, branch string, filePath string) (string, int, error) {
9871024
rGetContentOps := github.RepositoryContentGetOptions{Ref: branch}
9881025
fileContent, _, resp, err := ghPrClientDetails.GhClientPair.v3Client.Repositories.GetContents(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, filePath, &rGetContentOps)
989-
prom.InstrumentGhCall(resp)
9901026
if err != nil {
9911027
ghPrClientDetails.PrLogger.Errorf("Fail to get file:%s\n%v\n", err, resp)
992-
return "", err, resp.StatusCode
1028+
if resp == nil {
1029+
return "", 0, err
1030+
}
1031+
prom.InstrumentGhCall(resp)
1032+
return "", resp.StatusCode, err
1033+
} else {
1034+
prom.InstrumentGhCall(resp)
9931035
}
9941036
fileContentString, err := fileContent.GetContent()
9951037
if err != nil {
9961038
ghPrClientDetails.PrLogger.Errorf("Fail to serlize file:%s\n", err)
997-
return "", err, resp.StatusCode
1039+
return "", resp.StatusCode, err
9981040
}
999-
return fileContentString, nil, resp.StatusCode
1041+
return fileContentString, resp.StatusCode, nil
10001042
}

internal/pkg/githubapi/promotion.go

+3
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ func contains(s []string, str string) bool {
5151
}
5252

5353
func DetectDrift(ghPrClientDetails GhPrClientDetails) error {
54+
if ghPrClientDetails.Ctx.Err() != nil {
55+
return ghPrClientDetails.Ctx.Err()
56+
}
5457
diffOutputMap := make(map[string]string)
5558
defaultBranch, _ := ghPrClientDetails.GetDefaultBranch()
5659
config, err := GetInRepoConfig(ghPrClientDetails, defaultBranch)

0 commit comments

Comments
 (0)