github.com/argoproj/argo-cd/v3@v3.2.1/server/badge/badge.go (about) 1 package badge 2 3 import ( 4 "context" 5 "fmt" 6 "net/http" 7 "regexp" 8 "strconv" 9 "strings" 10 11 healthutil "github.com/argoproj/gitops-engine/pkg/health" 12 "k8s.io/apimachinery/pkg/api/errors" 13 "k8s.io/apimachinery/pkg/api/validation" 14 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 16 appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" 17 "github.com/argoproj/argo-cd/v3/pkg/client/clientset/versioned" 18 "github.com/argoproj/argo-cd/v3/util/argo" 19 "github.com/argoproj/argo-cd/v3/util/assets" 20 "github.com/argoproj/argo-cd/v3/util/security" 21 "github.com/argoproj/argo-cd/v3/util/settings" 22 ) 23 24 // NewHandler creates handler serving to do api/badge endpoint 25 func NewHandler(appClientset versioned.Interface, settingsMrg *settings.SettingsManager, namespace string, enabledNamespaces []string) http.Handler { 26 return &Handler{appClientset: appClientset, namespace: namespace, settingsMgr: settingsMrg, enabledNamespaces: enabledNamespaces} 27 } 28 29 // Handler used to get application in order to access health/sync 30 type Handler struct { 31 namespace string 32 appClientset versioned.Interface 33 settingsMgr *settings.SettingsManager 34 enabledNamespaces []string 35 } 36 37 var ( 38 svgWidthPattern = regexp.MustCompile(`^<svg width="([^"]*)"`) 39 displayNonePattern = regexp.MustCompile(`display="none"`) 40 leftRectColorPattern = regexp.MustCompile(`id="leftRect" fill="([^"]*)"`) 41 rightRectColorPattern = regexp.MustCompile(`id="rightRect" fill="([^"]*)"`) 42 revisionRectColorPattern = regexp.MustCompile(`id="revisionRect" fill="([^"]*)"`) 43 leftTextPattern = regexp.MustCompile(`id="leftText" [^>]*>([^<]*)`) 44 rightTextPattern = regexp.MustCompile(`id="rightText" [^>]*>([^<]*)`) 45 revisionTextPattern = regexp.MustCompile(`id="revisionText" [^>]*>([^<]*)`) 46 titleTextPattern = regexp.MustCompile(`id="titleText" [^>]*>([^<]*)`) 47 titleRectWidthPattern = regexp.MustCompile(`(id="titleRect" .* width=)("0")`) 48 rightRectWidthPattern = regexp.MustCompile(`(id="rightRect" .* width=)("\d*")`) 49 revisionRectWidthPattern = regexp.MustCompile(`(id="revisionRect" .* width=)("\d*")`) 50 leftRectYCoodPattern = regexp.MustCompile(`(id="leftRect" .* y=)("\d*")`) 51 rightRectYCoodPattern = regexp.MustCompile(`(id="rightRect" .* y=)("\d*")`) 52 revisionRectYCoodPattern = regexp.MustCompile(`(id="revisionRect" .* y=)("\d*")`) 53 leftTextYCoodPattern = regexp.MustCompile(`(id="leftText" .* y=)("\d*")`) 54 rightTextYCoodPattern = regexp.MustCompile(`(id="rightText" .* y=)("\d*")`) 55 revisionTextYCoodPattern = regexp.MustCompile(`(id="revisionText" .* y=)("\d*")`) 56 revisionTextXCoodPattern = regexp.MustCompile(`(id="revisionText" x=)("\d*")`) 57 svgHeightPattern = regexp.MustCompile(`^(<svg .* height=)("\d*")`) 58 logoYCoodPattern = regexp.MustCompile(`(<image .* y=)("\d*")`) 59 ) 60 61 const ( 62 svgWidthWithRevision = 192 63 svgWidthWithFullRevision = 400 64 svgWidthWithoutRevision = 131 65 svgHeightWithAppName = 40 66 badgeRowHeight = 20 67 statusRowYCoodWithAppName = 330 68 logoYCoodWithAppName = 22 69 leftRectWidth = 77 70 widthPerChar = 6 71 textPositionWidthPerChar = 62 72 ) 73 74 func replaceFirstGroupSubMatch(re *regexp.Regexp, str string, repl string) string { 75 result := "" 76 lastIndex := 0 77 78 for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) { 79 groups := []string{} 80 for i := 0; i < len(v); i += 2 { 81 groups = append(groups, str[v[i]:v[i+1]]) 82 } 83 84 result += str[lastIndex:v[0]] + groups[0] + repl 85 lastIndex = v[1] 86 } 87 88 return result + str[lastIndex:] 89 } 90 91 // ServeHTTP returns badge with health and sync status for application 92 // (or an error badge if wrong query or application name is given) 93 func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 94 health := healthutil.HealthStatusUnknown 95 status := appv1.SyncStatusCodeUnknown 96 revision := "" 97 displayedRevision := "" 98 applicationName := "" 99 revisionEnabled := false 100 enabled := false 101 displayAppName := false 102 notFound := false 103 adjustWidth := false 104 svgWidth := svgWidthWithoutRevision 105 if sets, err := h.settingsMgr.GetSettings(); err == nil { 106 enabled = sets.StatusBadgeEnabled 107 } 108 109 reqNs := "" 110 if ns, ok := r.URL.Query()["namespace"]; ok && enabled { 111 if !argo.IsValidNamespaceName(ns[0]) { 112 w.WriteHeader(http.StatusBadRequest) 113 return 114 } 115 if security.IsNamespaceEnabled(ns[0], h.namespace, h.enabledNamespaces) { 116 reqNs = ns[0] 117 } else { 118 notFound = true 119 } 120 } else { 121 reqNs = h.namespace 122 } 123 124 // Sample url: http://localhost:8080/api/badge?name=123 125 if name, ok := r.URL.Query()["name"]; ok && enabled && !notFound { 126 if !argo.IsValidAppName(name[0]) { 127 w.WriteHeader(http.StatusBadRequest) 128 return 129 } 130 if app, err := h.appClientset.ArgoprojV1alpha1().Applications(reqNs).Get(context.Background(), name[0], metav1.GetOptions{}); err == nil { 131 health = app.Status.Health.Status 132 status = app.Status.Sync.Status 133 applicationName = name[0] 134 if app.Status.OperationState != nil && app.Status.OperationState.SyncResult != nil { 135 if len(app.Status.OperationState.SyncResult.Revisions) > 0 { 136 revision = app.Status.OperationState.SyncResult.Revisions[0] 137 } else { 138 revision = app.Status.OperationState.SyncResult.Revision 139 } 140 } 141 } else if errors.IsNotFound(err) { 142 notFound = true 143 } 144 } 145 // Sample url: http://localhost:8080/api/badge?project=default 146 if projects, ok := r.URL.Query()["project"]; ok && enabled && !notFound { 147 for _, p := range projects { 148 if errs := validation.NameIsDNSLabel(strings.ToLower(p), false); p != "" && len(errs) != 0 { 149 w.WriteHeader(http.StatusBadRequest) 150 return 151 } 152 } 153 if apps, err := h.appClientset.ArgoprojV1alpha1().Applications(reqNs).List(context.Background(), metav1.ListOptions{}); err == nil { 154 applicationSet := argo.FilterByProjects(apps.Items, projects) 155 for _, a := range applicationSet { 156 if a.Status.Sync.Status != appv1.SyncStatusCodeSynced { 157 status = appv1.SyncStatusCodeOutOfSync 158 } 159 if a.Status.Health.Status != healthutil.HealthStatusHealthy { 160 health = healthutil.HealthStatusDegraded 161 } 162 } 163 if health != healthutil.HealthStatusDegraded && len(applicationSet) > 0 { 164 health = healthutil.HealthStatusHealthy 165 } 166 if status != appv1.SyncStatusCodeOutOfSync && len(applicationSet) > 0 { 167 status = appv1.SyncStatusCodeSynced 168 } 169 } 170 } 171 // Sample url: http://localhost:8080/api/badge?name=123&revision=true 172 if revisionParam, ok := r.URL.Query()["revision"]; ok && enabled && strings.EqualFold(revisionParam[0], "true") { 173 revisionEnabled = true 174 } 175 176 leftColorString := "" 177 if leftColor, ok := HealthStatusColors[health]; ok { 178 leftColorString = toRGBString(leftColor) 179 } else { 180 leftColorString = toRGBString(Grey) 181 } 182 183 rightColorString := "" 184 if rightColor, ok := SyncStatusColors[status]; ok { 185 rightColorString = toRGBString(rightColor) 186 } else { 187 rightColorString = toRGBString(Grey) 188 } 189 190 leftText := string(health) 191 rightText := string(status) 192 193 if notFound { 194 leftText = "Not Found" 195 rightText = "" 196 } 197 198 badge := assets.BadgeSVG 199 badge = leftRectColorPattern.ReplaceAllString(badge, fmt.Sprintf(`id="leftRect" fill=%q $2`, leftColorString)) 200 badge = rightRectColorPattern.ReplaceAllString(badge, fmt.Sprintf(`id="rightRect" fill=%q $2`, rightColorString)) 201 badge = replaceFirstGroupSubMatch(leftTextPattern, badge, leftText) 202 badge = replaceFirstGroupSubMatch(rightTextPattern, badge, rightText) 203 204 if !notFound && revisionEnabled && revision != "" { 205 // Enable display of revision components 206 badge = displayNonePattern.ReplaceAllString(badge, `display="inline"`) 207 badge = revisionRectColorPattern.ReplaceAllString(badge, fmt.Sprintf(`id="revisionRect" fill=%q $2`, rightColorString)) 208 209 adjustWidth = true 210 displayedRevision = revision 211 if keepFullRevisionParam, ok := r.URL.Query()["keepFullRevision"]; (!ok || !strings.EqualFold(keepFullRevisionParam[0], "true")) && len(revision) > 7 { 212 displayedRevision = revision[:7] 213 svgWidth = svgWidthWithRevision 214 } else { 215 svgWidth = svgWidthWithFullRevision 216 } 217 218 badge = replaceFirstGroupSubMatch(revisionTextPattern, badge, fmt.Sprintf("(%s)", displayedRevision)) 219 } 220 221 if widthParam, ok := r.URL.Query()["width"]; ok && enabled { 222 width, err := strconv.Atoi(widthParam[0]) 223 if err == nil { 224 svgWidth = width 225 adjustWidth = true 226 } 227 } 228 229 // Increase width of SVG 230 if adjustWidth { 231 badge = svgWidthPattern.ReplaceAllString(badge, fmt.Sprintf(`<svg width="%d" $2`, svgWidth)) 232 if revisionEnabled { 233 xpos := (svgWidthWithoutRevision)*10 + (len(displayedRevision)+1)*textPositionWidthPerChar/2 234 badge = revisionRectWidthPattern.ReplaceAllString(badge, fmt.Sprintf(`$1"%d"`, svgWidth-svgWidthWithoutRevision)) 235 badge = revisionTextXCoodPattern.ReplaceAllString(badge, fmt.Sprintf(`$1"%d"`, xpos)) 236 } else { 237 badge = rightRectWidthPattern.ReplaceAllString(badge, fmt.Sprintf(`$1"%d"`, svgWidth-leftRectWidth)) 238 } 239 } 240 241 if showAppNameParam, ok := r.URL.Query()["showAppName"]; ok && enabled && strings.EqualFold(showAppNameParam[0], "true") { 242 displayAppName = true 243 } 244 245 if displayAppName && applicationName != "" { 246 titleRectWidth := len(applicationName) * widthPerChar 247 longerWidth := max(titleRectWidth, svgWidth) 248 rightRectWidth := longerWidth - leftRectWidth 249 badge = titleRectWidthPattern.ReplaceAllString(badge, fmt.Sprintf(`$1"%d"`, longerWidth)) 250 badge = rightRectWidthPattern.ReplaceAllString(badge, fmt.Sprintf(`$1"%d"`, rightRectWidth)) 251 badge = replaceFirstGroupSubMatch(titleTextPattern, badge, applicationName) 252 badge = leftRectYCoodPattern.ReplaceAllString(badge, fmt.Sprintf(`$1"%d"`, badgeRowHeight)) 253 badge = rightRectYCoodPattern.ReplaceAllString(badge, fmt.Sprintf(`$1"%d"`, badgeRowHeight)) 254 badge = revisionRectYCoodPattern.ReplaceAllString(badge, fmt.Sprintf(`$1"%d"`, badgeRowHeight)) 255 badge = leftTextYCoodPattern.ReplaceAllString(badge, fmt.Sprintf(`$1"%d"`, statusRowYCoodWithAppName)) 256 badge = rightTextYCoodPattern.ReplaceAllString(badge, fmt.Sprintf(`$1"%d"`, statusRowYCoodWithAppName)) 257 badge = revisionTextYCoodPattern.ReplaceAllString(badge, fmt.Sprintf(`$1"%d"`, statusRowYCoodWithAppName)) 258 badge = svgHeightPattern.ReplaceAllString(badge, fmt.Sprintf(`$1"%d"`, svgHeightWithAppName)) 259 badge = logoYCoodPattern.ReplaceAllString(badge, fmt.Sprintf(`$1"%d"`, logoYCoodWithAppName)) 260 badge = svgWidthPattern.ReplaceAllString(badge, fmt.Sprintf(`<svg width="%d" $2`, longerWidth)) 261 } 262 263 w.Header().Set("Content-Type", "image/svg+xml") 264 265 // Ask cache's to not cache the contents in order prevent the badge from becoming stale 266 w.Header().Set("Cache-Control", "private, no-store") 267 268 // Allow badges to be fetched via XHR from frontend applications without running into CORS issues 269 w.Header().Set("Access-Control-Allow-Origin", "*") 270 w.WriteHeader(http.StatusOK) 271 _, _ = w.Write([]byte(badge)) 272 }