2016-11-09 20:30:07 +00:00
|
|
|
|
// Copyright 2016 Circonus, Inc. All rights reserved.
|
|
|
|
|
// Use of this source code is governed by a BSD-style
|
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
|
2016-07-19 23:40:41 +00:00
|
|
|
|
package checkmgr
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"crypto/rand"
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
2019-06-19 12:50:48 +00:00
|
|
|
|
"net/url"
|
2016-07-19 23:40:41 +00:00
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/circonus-labs/circonus-gometrics/api"
|
2019-06-19 12:50:48 +00:00
|
|
|
|
"github.com/circonus-labs/circonus-gometrics/api/config"
|
2016-07-19 23:40:41 +00:00
|
|
|
|
)
|
|
|
|
|
|
2016-11-09 20:30:07 +00:00
|
|
|
|
// UpdateCheck determines if the check needs to be updated (new metrics, tags, etc.)
|
|
|
|
|
func (cm *CheckManager) UpdateCheck(newMetrics map[string]*api.CheckBundleMetric) {
|
|
|
|
|
// only if check manager is enabled
|
|
|
|
|
if !cm.enabled {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// only if checkBundle has been populated
|
|
|
|
|
if cm.checkBundle == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// only if there is *something* to update
|
|
|
|
|
if !cm.forceCheckUpdate && len(newMetrics) == 0 && len(cm.metricTags) == 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// refresh check bundle (in case there were changes made by other apps or in UI)
|
2019-06-19 12:50:48 +00:00
|
|
|
|
cid := cm.checkBundle.CID
|
|
|
|
|
checkBundle, err := cm.apih.FetchCheckBundle(api.CIDType(&cid))
|
2016-11-09 20:30:07 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
cm.Log.Printf("[ERROR] unable to fetch up-to-date check bundle %v", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
cm.cbmu.Lock()
|
|
|
|
|
cm.checkBundle = checkBundle
|
|
|
|
|
cm.cbmu.Unlock()
|
|
|
|
|
|
2019-06-19 12:50:48 +00:00
|
|
|
|
// check metric_limit and see if it’s 0, if so, don't even bother to try to update the check.
|
|
|
|
|
|
2016-11-09 20:30:07 +00:00
|
|
|
|
cm.addNewMetrics(newMetrics)
|
|
|
|
|
|
|
|
|
|
if len(cm.metricTags) > 0 {
|
|
|
|
|
// note: if a tag has been added (queued) for a metric which never gets sent
|
|
|
|
|
// the tags will be discarded. (setting tags does not *create* metrics.)
|
|
|
|
|
for metricName, metricTags := range cm.metricTags {
|
|
|
|
|
for metricIdx, metric := range cm.checkBundle.Metrics {
|
|
|
|
|
if metric.Name == metricName {
|
|
|
|
|
cm.checkBundle.Metrics[metricIdx].Tags = metricTags
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
cm.mtmu.Lock()
|
|
|
|
|
delete(cm.metricTags, metricName)
|
|
|
|
|
cm.mtmu.Unlock()
|
|
|
|
|
}
|
|
|
|
|
cm.forceCheckUpdate = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if cm.forceCheckUpdate {
|
|
|
|
|
newCheckBundle, err := cm.apih.UpdateCheckBundle(cm.checkBundle)
|
|
|
|
|
if err != nil {
|
|
|
|
|
cm.Log.Printf("[ERROR] updating check bundle %v", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cm.forceCheckUpdate = false
|
|
|
|
|
cm.cbmu.Lock()
|
|
|
|
|
cm.checkBundle = newCheckBundle
|
|
|
|
|
cm.cbmu.Unlock()
|
|
|
|
|
cm.inventoryMetrics()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2016-07-19 23:40:41 +00:00
|
|
|
|
// Initialize CirconusMetrics instance. Attempt to find a check otherwise create one.
|
|
|
|
|
// use cases:
|
|
|
|
|
//
|
|
|
|
|
// check [bundle] by submission url
|
|
|
|
|
// check [bundle] by *check* id (note, not check_bundle id)
|
|
|
|
|
// check [bundle] by search
|
|
|
|
|
// create check [bundle]
|
|
|
|
|
func (cm *CheckManager) initializeTrapURL() error {
|
|
|
|
|
if cm.trapURL != "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cm.trapmu.Lock()
|
|
|
|
|
defer cm.trapmu.Unlock()
|
|
|
|
|
|
2016-11-09 20:30:07 +00:00
|
|
|
|
// special case short-circuit: just send to a url, no check management
|
|
|
|
|
// up to user to ensure that if url is https that it will work (e.g. not self-signed)
|
2016-07-19 23:40:41 +00:00
|
|
|
|
if cm.checkSubmissionURL != "" {
|
|
|
|
|
if !cm.enabled {
|
|
|
|
|
cm.trapURL = cm.checkSubmissionURL
|
|
|
|
|
cm.trapLastUpdate = time.Now()
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !cm.enabled {
|
2019-06-19 12:50:48 +00:00
|
|
|
|
return errors.New("unable to initialize trap, check manager is disabled")
|
2016-07-19 23:40:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var err error
|
|
|
|
|
var check *api.Check
|
|
|
|
|
var checkBundle *api.CheckBundle
|
|
|
|
|
var broker *api.Broker
|
|
|
|
|
|
|
|
|
|
if cm.checkSubmissionURL != "" {
|
2019-06-19 12:50:48 +00:00
|
|
|
|
check, err = cm.fetchCheckBySubmissionURL(cm.checkSubmissionURL)
|
2016-07-19 23:40:41 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2016-11-09 20:30:07 +00:00
|
|
|
|
if !check.Active {
|
2019-06-19 12:50:48 +00:00
|
|
|
|
return fmt.Errorf("[ERROR] Check ID %v is not active", check.CID)
|
2016-11-09 20:30:07 +00:00
|
|
|
|
}
|
2016-07-19 23:40:41 +00:00
|
|
|
|
// extract check id from check object returned from looking up using submission url
|
|
|
|
|
// set m.CheckId to the id
|
|
|
|
|
// set m.SubmissionUrl to "" to prevent trying to search on it going forward
|
|
|
|
|
// use case: if the broker is changed in the UI metrics would stop flowing
|
|
|
|
|
// unless the new submission url can be fetched with the API (which is no
|
|
|
|
|
// longer possible using the original submission url)
|
|
|
|
|
var id int
|
2019-06-19 12:50:48 +00:00
|
|
|
|
id, err = strconv.Atoi(strings.Replace(check.CID, "/check/", "", -1))
|
2016-07-19 23:40:41 +00:00
|
|
|
|
if err == nil {
|
|
|
|
|
cm.checkID = api.IDType(id)
|
|
|
|
|
cm.checkSubmissionURL = ""
|
|
|
|
|
} else {
|
|
|
|
|
cm.Log.Printf(
|
|
|
|
|
"[WARN] SubmissionUrl check to Check ID: unable to convert %s to int %q\n",
|
2019-06-19 12:50:48 +00:00
|
|
|
|
check.CID, err)
|
2016-07-19 23:40:41 +00:00
|
|
|
|
}
|
|
|
|
|
} else if cm.checkID > 0 {
|
2019-06-19 12:50:48 +00:00
|
|
|
|
cid := fmt.Sprintf("/check/%d", cm.checkID)
|
|
|
|
|
check, err = cm.apih.FetchCheck(api.CIDType(&cid))
|
2016-07-19 23:40:41 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2016-11-09 20:30:07 +00:00
|
|
|
|
if !check.Active {
|
2019-06-19 12:50:48 +00:00
|
|
|
|
return fmt.Errorf("[ERROR] Check ID %v is not active", check.CID)
|
2016-11-09 20:30:07 +00:00
|
|
|
|
}
|
2016-07-19 23:40:41 +00:00
|
|
|
|
} else {
|
2019-06-19 12:50:48 +00:00
|
|
|
|
if checkBundle == nil {
|
|
|
|
|
// old search (instanceid as check.target)
|
|
|
|
|
searchCriteria := fmt.Sprintf(
|
|
|
|
|
"(active:1)(type:\"%s\")(host:\"%s\")(tags:%s)", cm.checkType, cm.checkTarget, strings.Join(cm.checkSearchTag, ","))
|
|
|
|
|
checkBundle, err = cm.checkBundleSearch(searchCriteria, map[string][]string{})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if checkBundle == nil {
|
|
|
|
|
// new search (check.target != instanceid, instanceid encoded in notes field)
|
|
|
|
|
searchCriteria := fmt.Sprintf(
|
|
|
|
|
"(active:1)(type:\"%s\")(tags:%s)", cm.checkType, strings.Join(cm.checkSearchTag, ","))
|
|
|
|
|
filterCriteria := map[string][]string{"f_notes": {*cm.getNotes()}}
|
|
|
|
|
checkBundle, err = cm.checkBundleSearch(searchCriteria, filterCriteria)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2016-07-19 23:40:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if checkBundle == nil {
|
|
|
|
|
// err==nil && checkBundle==nil is "no check bundles matched"
|
|
|
|
|
// an error *should* be returned for any other invalid scenario
|
|
|
|
|
checkBundle, broker, err = cm.createNewCheck()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if checkBundle == nil {
|
|
|
|
|
if check != nil {
|
2019-06-19 12:50:48 +00:00
|
|
|
|
cid := check.CheckBundleCID
|
|
|
|
|
checkBundle, err = cm.apih.FetchCheckBundle(api.CIDType(&cid))
|
2016-07-19 23:40:41 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return fmt.Errorf("[ERROR] Unable to retrieve, find, or create check")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if broker == nil {
|
2019-06-19 12:50:48 +00:00
|
|
|
|
cid := checkBundle.Brokers[0]
|
|
|
|
|
broker, err = cm.apih.FetchBroker(api.CIDType(&cid))
|
2016-07-19 23:40:41 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// retain to facilitate metric management (adding new metrics specifically)
|
|
|
|
|
cm.checkBundle = checkBundle
|
|
|
|
|
cm.inventoryMetrics()
|
|
|
|
|
|
2016-11-09 20:30:07 +00:00
|
|
|
|
// determine the trap url to which metrics should be PUT
|
|
|
|
|
if checkBundle.Type == "httptrap" {
|
2019-06-19 12:50:48 +00:00
|
|
|
|
if turl, found := checkBundle.Config[config.SubmissionURL]; found {
|
|
|
|
|
cm.trapURL = api.URLType(turl)
|
|
|
|
|
} else {
|
|
|
|
|
if cm.Debug {
|
|
|
|
|
cm.Log.Printf("Missing config.%s %+v", config.SubmissionURL, checkBundle)
|
|
|
|
|
}
|
|
|
|
|
return fmt.Errorf("[ERROR] Unable to use check, no %s in config", config.SubmissionURL)
|
|
|
|
|
}
|
2016-11-09 20:30:07 +00:00
|
|
|
|
} else {
|
|
|
|
|
// build a submission_url for non-httptrap checks out of mtev_reverse url
|
|
|
|
|
if len(checkBundle.ReverseConnectURLs) == 0 {
|
|
|
|
|
return fmt.Errorf("%s is not an HTTPTRAP check and no reverse connection urls found", checkBundle.Checks[0])
|
|
|
|
|
}
|
|
|
|
|
mtevURL := checkBundle.ReverseConnectURLs[0]
|
|
|
|
|
mtevURL = strings.Replace(mtevURL, "mtev_reverse", "https", 1)
|
|
|
|
|
mtevURL = strings.Replace(mtevURL, "check", "module/httptrap", 1)
|
2019-06-19 12:50:48 +00:00
|
|
|
|
if rs, found := checkBundle.Config[config.ReverseSecretKey]; found {
|
|
|
|
|
cm.trapURL = api.URLType(fmt.Sprintf("%s/%s", mtevURL, rs))
|
|
|
|
|
} else {
|
|
|
|
|
if cm.Debug {
|
|
|
|
|
cm.Log.Printf("Missing config.%s %+v", config.ReverseSecretKey, checkBundle)
|
|
|
|
|
}
|
|
|
|
|
return fmt.Errorf("[ERROR] Unable to use check, no %s in config", config.ReverseSecretKey)
|
|
|
|
|
}
|
2016-11-09 20:30:07 +00:00
|
|
|
|
}
|
2016-07-19 23:40:41 +00:00
|
|
|
|
|
|
|
|
|
// used when sending as "ServerName" get around certs not having IP SANS
|
|
|
|
|
// (cert created with server name as CN but IP used in trap url)
|
|
|
|
|
cn, err := cm.getBrokerCN(broker, cm.trapURL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
cm.trapCN = BrokerCNType(cn)
|
|
|
|
|
|
2019-06-19 12:50:48 +00:00
|
|
|
|
if cm.enabled {
|
|
|
|
|
u, err := url.Parse(string(cm.trapURL))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if u.Scheme == "https" {
|
|
|
|
|
if err := cm.loadCACert(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-07-19 23:40:41 +00:00
|
|
|
|
cm.trapLastUpdate = time.Now()
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Search for a check bundle given a predetermined set of criteria
|
2019-06-19 12:50:48 +00:00
|
|
|
|
func (cm *CheckManager) checkBundleSearch(criteria string, filter map[string][]string) (*api.CheckBundle, error) {
|
|
|
|
|
search := api.SearchQueryType(criteria)
|
|
|
|
|
checkBundles, err := cm.apih.SearchCheckBundles(&search, &filter)
|
2016-07-19 23:40:41 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-19 12:50:48 +00:00
|
|
|
|
if len(*checkBundles) == 0 {
|
2016-07-19 23:40:41 +00:00
|
|
|
|
return nil, nil // trigger creation of a new check
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
numActive := 0
|
|
|
|
|
checkID := -1
|
|
|
|
|
|
2019-06-19 12:50:48 +00:00
|
|
|
|
for idx, check := range *checkBundles {
|
2016-07-19 23:40:41 +00:00
|
|
|
|
if check.Status == statusActive {
|
|
|
|
|
numActive++
|
|
|
|
|
checkID = idx
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if numActive > 1 {
|
2019-06-19 12:50:48 +00:00
|
|
|
|
return nil, fmt.Errorf("[ERROR] multiple check bundles match criteria %s", criteria)
|
2016-07-19 23:40:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-19 12:50:48 +00:00
|
|
|
|
bundle := (*checkBundles)[checkID]
|
|
|
|
|
|
|
|
|
|
return &bundle, nil
|
2016-07-19 23:40:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create a new check to receive metrics
|
|
|
|
|
func (cm *CheckManager) createNewCheck() (*api.CheckBundle, *api.Broker, error) {
|
|
|
|
|
checkSecret := string(cm.checkSecret)
|
|
|
|
|
if checkSecret == "" {
|
|
|
|
|
secret, err := cm.makeSecret()
|
|
|
|
|
if err != nil {
|
|
|
|
|
secret = "myS3cr3t"
|
|
|
|
|
}
|
|
|
|
|
checkSecret = secret
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
broker, err := cm.getBroker()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-19 12:50:48 +00:00
|
|
|
|
chkcfg := &api.CheckBundle{
|
|
|
|
|
Brokers: []string{broker.CID},
|
|
|
|
|
Config: make(map[config.Key]string),
|
2016-07-19 23:40:41 +00:00
|
|
|
|
DisplayName: string(cm.checkDisplayName),
|
|
|
|
|
Metrics: []api.CheckBundleMetric{},
|
2019-06-19 12:50:48 +00:00
|
|
|
|
MetricLimit: config.DefaultCheckBundleMetricLimit,
|
|
|
|
|
Notes: cm.getNotes(),
|
2016-07-19 23:40:41 +00:00
|
|
|
|
Period: 60,
|
|
|
|
|
Status: statusActive,
|
2016-11-09 20:30:07 +00:00
|
|
|
|
Tags: append(cm.checkSearchTag, cm.checkTags...),
|
2019-06-19 12:50:48 +00:00
|
|
|
|
Target: string(cm.checkTarget),
|
2016-07-19 23:40:41 +00:00
|
|
|
|
Timeout: 10,
|
|
|
|
|
Type: string(cm.checkType),
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-19 12:50:48 +00:00
|
|
|
|
if len(cm.customConfigFields) > 0 {
|
|
|
|
|
for fld, val := range cm.customConfigFields {
|
|
|
|
|
chkcfg.Config[config.Key(fld)] = val
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
|
// use the default config settings if these are NOT set by user configuration
|
|
|
|
|
//
|
|
|
|
|
if val, ok := chkcfg.Config[config.AsyncMetrics]; !ok || val == "" {
|
|
|
|
|
chkcfg.Config[config.AsyncMetrics] = "true"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if val, ok := chkcfg.Config[config.Secret]; !ok || val == "" {
|
|
|
|
|
chkcfg.Config[config.Secret] = checkSecret
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
checkBundle, err := cm.apih.CreateCheckBundle(chkcfg)
|
2016-07-19 23:40:41 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return checkBundle, broker, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create a dynamic secret to use with a new check
|
|
|
|
|
func (cm *CheckManager) makeSecret() (string, error) {
|
|
|
|
|
hash := sha256.New()
|
|
|
|
|
x := make([]byte, 2048)
|
|
|
|
|
if _, err := rand.Read(x); err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
hash.Write(x)
|
|
|
|
|
return hex.EncodeToString(hash.Sum(nil))[0:16], nil
|
|
|
|
|
}
|
2019-06-19 12:50:48 +00:00
|
|
|
|
|
|
|
|
|
func (cm *CheckManager) getNotes() *string {
|
|
|
|
|
notes := fmt.Sprintf("cgm_instanceid|%s", cm.checkInstanceID)
|
|
|
|
|
return ¬es
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FetchCheckBySubmissionURL fetch a check configuration by submission_url
|
|
|
|
|
func (cm *CheckManager) fetchCheckBySubmissionURL(submissionURL api.URLType) (*api.Check, error) {
|
|
|
|
|
if string(submissionURL) == "" {
|
|
|
|
|
return nil, errors.New("[ERROR] Invalid submission URL (blank)")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
u, err := url.Parse(string(submissionURL))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// valid trap url: scheme://host[:port]/module/httptrap/UUID/secret
|
|
|
|
|
|
|
|
|
|
// does it smell like a valid trap url path
|
|
|
|
|
if !strings.Contains(u.Path, "/module/httptrap/") {
|
|
|
|
|
return nil, fmt.Errorf("[ERROR] Invalid submission URL '%s', unrecognized path", submissionURL)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// extract uuid
|
|
|
|
|
pathParts := strings.Split(strings.Replace(u.Path, "/module/httptrap/", "", 1), "/")
|
|
|
|
|
if len(pathParts) != 2 {
|
|
|
|
|
return nil, fmt.Errorf("[ERROR] Invalid submission URL '%s', UUID not where expected", submissionURL)
|
|
|
|
|
}
|
|
|
|
|
uuid := pathParts[0]
|
|
|
|
|
|
|
|
|
|
filter := api.SearchFilterType{"f__check_uuid": []string{uuid}}
|
|
|
|
|
|
|
|
|
|
checks, err := cm.apih.SearchChecks(nil, &filter)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(*checks) == 0 {
|
|
|
|
|
return nil, fmt.Errorf("[ERROR] No checks found with UUID %s", uuid)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
numActive := 0
|
|
|
|
|
checkID := -1
|
|
|
|
|
|
|
|
|
|
for idx, check := range *checks {
|
|
|
|
|
if check.Active {
|
|
|
|
|
numActive++
|
|
|
|
|
checkID = idx
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if numActive > 1 {
|
|
|
|
|
return nil, fmt.Errorf("[ERROR] Multiple checks with same UUID %s", uuid)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
check := (*checks)[checkID]
|
|
|
|
|
|
|
|
|
|
return &check, nil
|
|
|
|
|
|
|
|
|
|
}
|