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