github.com/argoproj/argo-cd@v1.8.7/server/badge/badge.go (about) 1 package badge 2 3 import ( 4 "context" 5 "fmt" 6 "net/http" 7 "regexp" 8 9 healthutil "github.com/argoproj/gitops-engine/pkg/health" 10 "k8s.io/apimachinery/pkg/api/errors" 11 v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 13 appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1" 14 "github.com/argoproj/argo-cd/pkg/client/clientset/versioned" 15 "github.com/argoproj/argo-cd/util/argo" 16 "github.com/argoproj/argo-cd/util/assets" 17 "github.com/argoproj/argo-cd/util/settings" 18 ) 19 20 //NewHandler creates handler serving to do api/badge endpoint 21 func NewHandler(appClientset versioned.Interface, settingsMrg *settings.SettingsManager, namespace string) http.Handler { 22 return &Handler{appClientset: appClientset, namespace: namespace, settingsMgr: settingsMrg} 23 } 24 25 //Handler used to get application in order to access health/sync 26 type Handler struct { 27 namespace string 28 appClientset versioned.Interface 29 settingsMgr *settings.SettingsManager 30 } 31 32 var ( 33 svgWidthPattern = regexp.MustCompile(`^<svg width="([^"]*)"`) 34 displayNonePattern = regexp.MustCompile(`display="none"`) 35 leftRectColorPattern = regexp.MustCompile(`id="leftRect" fill="([^"]*)"`) 36 rightRectColorPattern = regexp.MustCompile(`id="rightRect" fill="([^"]*)"`) 37 revisionRectColorPattern = regexp.MustCompile(`id="revisionRect" fill="([^"]*)"`) 38 leftTextPattern = regexp.MustCompile(`id="leftText" [^>]*>([^<]*)`) 39 rightTextPattern = regexp.MustCompile(`id="rightText" [^>]*>([^<]*)`) 40 revisionTextPattern = regexp.MustCompile(`id="revisionText" [^>]*>([^<]*)`) 41 ) 42 43 const ( 44 svgWidthWithRevision = 192 45 ) 46 47 func replaceFirstGroupSubMatch(re *regexp.Regexp, str string, repl string) string { 48 result := "" 49 lastIndex := 0 50 51 for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) { 52 groups := []string{} 53 for i := 0; i < len(v); i += 2 { 54 groups = append(groups, str[v[i]:v[i+1]]) 55 } 56 57 result += str[lastIndex:v[0]] + groups[0] + repl 58 lastIndex = v[1] 59 } 60 61 return result + str[lastIndex:] 62 } 63 64 //ServeHTTP returns badge with health and sync status for application 65 //(or an error badge if wrong query or application name is given) 66 func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 67 health := healthutil.HealthStatusUnknown 68 status := appv1.SyncStatusCodeUnknown 69 revision := "" 70 revisionEnabled := false 71 enabled := false 72 notFound := false 73 if sets, err := h.settingsMgr.GetSettings(); err == nil { 74 enabled = sets.StatusBadgeEnabled 75 } 76 77 //Sample url: http://localhost:8080/api/badge?name=123 78 if name, ok := r.URL.Query()["name"]; ok && enabled { 79 if app, err := h.appClientset.ArgoprojV1alpha1().Applications(h.namespace).Get(context.Background(), name[0], v1.GetOptions{}); err == nil { 80 health = app.Status.Health.Status 81 status = app.Status.Sync.Status 82 if app.Status.OperationState != nil && app.Status.OperationState.SyncResult != nil { 83 revision = app.Status.OperationState.SyncResult.Revision 84 } 85 } else if errors.IsNotFound(err) { 86 notFound = true 87 } 88 } 89 //Sample url: http://localhost:8080/api/badge?project=default 90 if projects, ok := r.URL.Query()["project"]; ok && enabled { 91 if apps, err := h.appClientset.ArgoprojV1alpha1().Applications(h.namespace).List(context.Background(), v1.ListOptions{}); err == nil { 92 applicationSet := argo.FilterByProjects(apps.Items, projects) 93 for _, a := range applicationSet { 94 if a.Status.Sync.Status != appv1.SyncStatusCodeSynced { 95 status = appv1.SyncStatusCodeOutOfSync 96 } 97 if a.Status.Health.Status != healthutil.HealthStatusHealthy { 98 health = healthutil.HealthStatusDegraded 99 } 100 } 101 if health != healthutil.HealthStatusDegraded && len(applicationSet) > 0 { 102 health = healthutil.HealthStatusHealthy 103 } 104 if status != appv1.SyncStatusCodeOutOfSync && len(applicationSet) > 0 { 105 status = appv1.SyncStatusCodeSynced 106 } 107 } 108 } 109 //Sample url: http://localhost:8080/api/badge?name=123&revision=true 110 if _, ok := r.URL.Query()["revision"]; ok && enabled { 111 revisionEnabled = true 112 } 113 114 leftColorString := "" 115 if leftColor, ok := HealthStatusColors[health]; ok { 116 leftColorString = toRGBString(leftColor) 117 } else { 118 leftColorString = toRGBString(Grey) 119 } 120 121 rightColorString := "" 122 if rightColor, ok := SyncStatusColors[status]; ok { 123 rightColorString = toRGBString(rightColor) 124 } else { 125 rightColorString = toRGBString(Grey) 126 } 127 128 leftText := string(health) 129 rightText := string(status) 130 131 if notFound { 132 leftText = "Not Found" 133 rightText = "" 134 } 135 136 badge := assets.BadgeSVG 137 badge = leftRectColorPattern.ReplaceAllString(badge, fmt.Sprintf(`id="leftRect" fill="%s" $2`, leftColorString)) 138 badge = rightRectColorPattern.ReplaceAllString(badge, fmt.Sprintf(`id="rightRect" fill="%s" $2`, rightColorString)) 139 badge = replaceFirstGroupSubMatch(leftTextPattern, badge, leftText) 140 badge = replaceFirstGroupSubMatch(rightTextPattern, badge, rightText) 141 142 if !notFound && revisionEnabled && revision != "" { 143 // Increase width of SVG and enable display of revision components 144 badge = svgWidthPattern.ReplaceAllString(badge, fmt.Sprintf(`<svg width="%d" $2`, svgWidthWithRevision)) 145 badge = displayNonePattern.ReplaceAllString(badge, `display="inline"`) 146 badge = revisionRectColorPattern.ReplaceAllString(badge, fmt.Sprintf(`id="revisionRect" fill="%s" $2`, rightColorString)) 147 shortRevision := revision 148 if len(shortRevision) > 7 { 149 shortRevision = shortRevision[:7] 150 } 151 badge = replaceFirstGroupSubMatch(revisionTextPattern, badge, fmt.Sprintf("(%s)", shortRevision)) 152 } 153 154 w.Header().Set("Content-Type", "image/svg+xml") 155 156 //Ask cache's to not cache the contents in order prevent the badge from becoming stale 157 w.Header().Set("Cache-Control", "private, no-store") 158 w.WriteHeader(http.StatusOK) 159 _, _ = w.Write([]byte(badge)) 160 }