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  }