diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..d125dc6 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,96 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: + - main + - dev + - feature/** + pull_request: + branches: + - main + - dev + schedule: + - cron: '20 3 * * 1' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: 'go' + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..afaf360 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.0.0 \ No newline at end of file diff --git a/bamboo_exporter.go b/bamboo_exporter.go new file mode 100644 index 0000000..3cb3e06 --- /dev/null +++ b/bamboo_exporter.go @@ -0,0 +1,96 @@ +package main + +import ( + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/EIETS/bamboo_exporter/collector" + "github.com/alecthomas/kingpin/v2" + "github.com/prometheus/client_golang/prometheus" + versioncollector "github.com/prometheus/client_golang/prometheus/collectors/version" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/prometheus/common/promslog" + "github.com/prometheus/common/promslog/flag" + "github.com/prometheus/common/version" + "github.com/prometheus/exporter-toolkit/web" + "github.com/prometheus/exporter-toolkit/web/kingpinflag" +) + +var ( + metricsEndpoint = kingpin.Flag("telemetry.endpoint", "Path under which to expose metrics.").Default("/metrics").String() + scrapeURI = kingpin.Flag("bamboo.uri", "Full Bamboo URI to scrape metrics from.").Default("http://localhost:8085").String() + insecure = kingpin.Flag("insecure", "Ignore server certificate if using https.").Bool() + // toolkitFlags: Add default web server configuration flags. + toolkitFlags = kingpinflag.AddFlags(kingpin.CommandLine, ":9117") + // gracefulStop: Channel to receive OS signals for graceful shutdown. + gracefulStop = make(chan os.Signal, 1) +) + +func main() { + promslogConfig := &promslog.Config{} + + // Parse flags + flag.AddFlags(kingpin.CommandLine, promslogConfig) + kingpin.HelpFlag.Short('h') + kingpin.Version(version.Print("bamboo_exporter")) + kingpin.Parse() + + logger := promslog.New(promslogConfig) + // listen to termination signals from the OS + signal.Notify(gracefulStop, syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP, syscall.SIGQUIT) + + config := &collector.Config{ + ScrapeURI: *scrapeURI, + Insecure: *insecure, + } + + exporter := collector.NewExporter(config, logger) + prometheus.MustRegister(exporter) + prometheus.MustRegister(versioncollector.NewCollector("bamboo_exporter")) + + // log startup information + logger.Info("Starting bamboo_exporter", "version", version.Info()) + logger.Info("Build context", "build", version.BuildContext()) + logger.Info("Collecting metrics from", "scrape_host", *scrapeURI) + + // listener for the termination signals from the OS + go func() { + logger.Debug("Listening and waiting for graceful stop") + sig := <-gracefulStop + logger.Info("Caught signal. Wait 2 seconds...", "sig", sig) + time.Sleep(2 * time.Second) + os.Exit(0) + }() + + http.Handle(*metricsEndpoint, promhttp.Handler()) + + // configure the landing page + landingConfig := web.LandingConfig{ + Name: "Bamboo Exporter", + Description: "Prometheus exporter for Bamboo metrics", + Version: version.Info(), + Links: []web.LandingLinks{ + { + Address: *metricsEndpoint, + Text: "Metrics", + }, + }, + } + + landingPage, err := web.NewLandingPage(landingConfig) + if err != nil { + logger.Error(err.Error()) + os.Exit(1) + } + http.Handle("/", landingPage) + + // start the http server + server := &http.Server{} + if err := web.ListenAndServe(server, toolkitFlags, logger); err != nil { + logger.Error(err.Error()) + os.Exit(1) + } +} diff --git a/collector/collector.go b/collector/collector.go new file mode 100644 index 0000000..e3ea619 --- /dev/null +++ b/collector/collector.go @@ -0,0 +1,220 @@ +package collector + +import ( + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "sync" + + "log/slog" + + "github.com/prometheus/client_golang/prometheus" +) + +const namespace = "bamboo" + +// Exporter collects metrics from Bamboo and exposes them to Prometheus. +type Exporter struct { + URI string + client *http.Client + mutex sync.Mutex + up *prometheus.Desc + failures prometheus.Counter + agents *prometheus.GaugeVec + queue prometheus.Gauge + utilization prometheus.Gauge + queueChange prometheus.Gauge + logger *slog.Logger + previousQueueSize int64 +} + +// Config holds the configuration for the exporter. +type Config struct { + ScrapeURI string + Insecure bool +} + +// BambooAgent represents an agent's data fetched from the Bamboo API. +type BambooAgent struct { + ID int64 `json:"id"` + Name string `json:"name"` + IsActive bool `json:"active"` + IsBusy bool `json:"busy"` +} + +// BambooQueue represents the build queue data fetched from the Bamboo API. +type BambooQueue struct { + QueuedBuilds struct { + Size int64 `json:"size"` + } `json:"queuedBuilds"` +} + +// NewExporter creates a new instance of Exporter. +func NewExporter(config *Config, logger *slog.Logger) *Exporter { + return &Exporter{ + URI: config.ScrapeURI, + client: &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: config.Insecure}, + }, + }, + up: prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "up"), + "Whether the Bamboo API is reachable.", + nil, + nil, + ), + failures: prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: namespace, + Name: "scrape_failures_total", + Help: "Total number of scrape failures.", + }), + agents: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "agents_status", + Help: "Status of Bamboo agents (active/busy).", + }, []string{"name", "status"}), + queue: prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "queue_size", + Help: "Number of builds in the Bamboo queue.", + }), + utilization: prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "agent_utilization", + Help: "Utilization rate of Bamboo agents (busy/active ratio).", + }), + queueChange: prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "queue_change", + Help: "Change in the Bamboo build queue size since the last scrape.", + }), + logger: logger, + } +} + +// Describe describes the Prometheus metrics for the exporter. +func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { + ch <- e.up + e.failures.Describe(ch) + e.agents.Describe(ch) + e.queue.Describe(ch) + e.utilization.Describe(ch) + e.queueChange.Describe(ch) +} + +// Collect collects metrics from Bamboo and sends them to Prometheus. +func (e *Exporter) Collect(ch chan<- prometheus.Metric) { + e.mutex.Lock() + defer e.mutex.Unlock() + + success := e.scrapeMetrics(ch) + ch <- prometheus.MustNewConstMetric(e.up, prometheus.GaugeValue, success) + e.failures.Collect(ch) + e.agents.Collect(ch) + e.queue.Collect(ch) + e.utilization.Collect(ch) + e.queueChange.Collect(ch) +} + +// scrapeMetrics fetches metrics from Bamboo and processes them. +func (e *Exporter) scrapeMetrics(ch chan<- prometheus.Metric) float64 { + if err := e.scrapeAgents(); err != nil { + e.logger.Error("Failed to scrape agents", "error", err) + e.failures.Inc() + return 0 + } + + if err := e.scrapeQueue(); err != nil { + e.logger.Error("Failed to scrape queue", "error", err) + e.failures.Inc() + return 0 + } + + return 1 +} + +// scrapeAgents fetches and processes agent metrics from Bamboo. +func (e *Exporter) scrapeAgents() error { + data, err := e.doRequest("/rest/api/latest/agent") + if err != nil { + return fmt.Errorf("error fetching agents: %w", err) + } + + var agents []BambooAgent + if err := json.Unmarshal(data, &agents); err != nil { + return fmt.Errorf("error unmarshaling agents: %w", err) + } + + e.agents.Reset() + activeCount := 0 + busyCount := 0 + + for _, agent := range agents { + status := "inactive" + if agent.IsActive { + status = "active" + activeCount++ + } + e.agents.WithLabelValues(agent.Name, status).Set(1) + + if agent.IsBusy { + e.agents.WithLabelValues(agent.Name, "busy").Set(1) + busyCount++ + } + } + + if activeCount > 0 { + utilization := float64(busyCount) / float64(activeCount) + e.utilization.Set(utilization) + } + + return nil +} + +// scrapeQueue fetches and processes queue metrics from Bamboo. +func (e *Exporter) scrapeQueue() error { + data, err := e.doRequest("/rest/api/latest/queue") + if err != nil { + return fmt.Errorf("error fetching queue: %w", err) + } + + var queue BambooQueue + if err := json.Unmarshal(data, &queue); err != nil { + return fmt.Errorf("error unmarshaling queue: %w", err) + } + + currentQueueSize := queue.QueuedBuilds.Size + e.queue.Set(float64(currentQueueSize)) + + // Calculate queue size change + change := currentQueueSize - e.previousQueueSize + e.queueChange.Set(float64(change)) + e.previousQueueSize = currentQueueSize + + return nil +} + +// doRequest sends a GET request to the Bamboo API and returns the response body. +func (e *Exporter) doRequest(endpoint string) ([]byte, error) { + req, err := http.NewRequest("GET", e.URI+endpoint, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + req.Header.Set("Accept", "application/json") + resp, err := e.client.Do(req) + if err != nil { + return nil, fmt.Errorf("error performing request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, errors.New("unexpected status code: " + resp.Status) + } + + return io.ReadAll(resp.Body) +} diff --git a/go.mod b/go.mod index 34ef9cd..a0ab7fc 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,34 @@ -module github.com/EIETS/bamboo-prometheus-exporter +module github.com/EIETS/bamboo_exporter go 1.23.1 + +require ( + github.com/alecthomas/kingpin/v2 v2.4.0 + github.com/prometheus/client_golang v1.20.4 + github.com/prometheus/common v0.60.0 + github.com/prometheus/exporter-toolkit v0.13.0 +) + +require ( + github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/jpillora/backoff v1.0.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/mdlayher/socket v0.4.1 // indirect + github.com/mdlayher/vsock v1.2.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/xhit/go-str2duration/v2 v2.1.0 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/net v0.29.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6a9c796 --- /dev/null +++ b/go.sum @@ -0,0 +1,76 @@ +github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= +github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= +github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= +github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ= +github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= +github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJNllA= +github.com/prometheus/common v0.60.0/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= +github.com/prometheus/exporter-toolkit v0.13.0 h1:lmA0Q+8IaXgmFRKw09RldZmZdnvu9wwcDLIXGmTPw1c= +github.com/prometheus/exporter-toolkit v0.13.0/go.mod h1:2uop99EZl80KdXhv/MxVI2181fMcwlsumFOqBecGkG0= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= +github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=