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  }