Compare commits

...

8 Commits

Author SHA1 Message Date
Christian Petersen d8eaf32582
Fix broken link to Datadog plugin in README.md (#63)
Signed-off-by: Christian Petersen <Christian.Petersen2@ibm.com>
2025-04-03 10:22:49 -04:00
Alex Meijer 42fa6c2496
skip empty plugins (#57)
Signed-off-by: Alex Meijer <ameijer@kubecost.com>
2024-10-29 20:18:54 +00:00
Alex Meijer 4c5d648738
overhaul DD plugin to use billed costs only (#56)
* overhaul DD plugin to use billed costs only

Signed-off-by: Alex Meijer <ameijer@kubecost.com>

* add missing import

Signed-off-by: Alex Meijer <ameijer@kubecost.com>

---------

Signed-off-by: Alex Meijer <ameijer@kubecost.com>
2024-10-29 16:02:22 -04:00
Alex Meijer b60c571ab9
get openAI and mongo integration tests working (#55)
Signed-off-by: Alex Meijer <ameijer@kubecost.com>
2024-10-23 10:30:08 -04:00
Sajit Mathew Kunnumkal 5ebd41de6d
Clean up code; add more tests (#44)
Signed-off-by: Sajit Kunnumkal <sajit@kunnumkal.com>
Co-authored-by: sajit <sajit@kunnumkal.com>
2024-10-17 09:25:33 -04:00
Alex Meijer 39e7d175b0
add contributors list (#43)
* add contributors list

Signed-off-by: Alex Meijer <ameijer@kubecost.com>

* tuning

Signed-off-by: Alex Meijer <ameijer@kubecost.com>

---------

Signed-off-by: Alex Meijer <ameijer@kubecost.com>
2024-10-16 16:17:07 -04:00
github-actions[bot] 173d98518d update manifest 2024-10-16 18:26:45 +00:00
Sajit Mathew Kunnumkal 5b117bca5e
[Atlas] integrate plugin Signed-off-by: Sajit Kunnumkal <sajit@kunnumkal.com> (#41)
Co-authored-by: sajit <sajit@kunnumkal.com>
2024-10-16 14:26:33 -04:00
18 changed files with 2196 additions and 395 deletions

View File

@ -25,20 +25,20 @@ At the most basic level, all a plugin needs to do is gather cost data given a ti
## Plugin setup ## Plugin setup
- Pull the repo locally. - Pull the repo locally.
- Create a directory within the repo for the new plugin. Check out the [Datadog plugin](https://github.com/opencost/opencost-plugins/tree/main/datadog) for a reference to follow along with. - Create a directory within the repo for the new plugin. Check out the [Datadog plugin](https://github.com/opencost/opencost-plugins/tree/main/pkg/plugins/datadog) for a reference to follow along with.
- Create the plugin subdirectories: - Create the plugin subdirectories:
- `<repo>/<plugin>/cmd/main/` - `<repo>/pkg/plugins/<plugin>/cmd/main/`
- This will contain the actual logic of the plugin. - This will contain the actual logic of the plugin.
- `<repo>/<plugin>/<plugin>plugin/` - `<repo>/pkg/plugins/<plugin>/<plugin>plugin/`
- This will contain the plugin config struct and any other structs for handling requests/responses in the plugin. - This will contain the plugin config struct and any other structs for handling requests/responses in the plugin.
- `<repo>/<plugin>/tests/` - `<repo>/pkg/plugins/<plugin>/tests/`
- Highly recommended, this will contain tests to validate the functionality of the plugin. - Highly recommended, this will contain tests to validate the functionality of the plugin.
- Initialize the subproject: - Initialize the subproject:
- Within `<repo>/<plugin>/`, run `go mod init github.com/opencost/opencost-plugins/<plugin>` and `go get github.com/hashicorp/go-plugin`. - Within `<<repo>/pkg/plugins/<plugin>/`, run `go mod init github.com/opencost/opencost-plugins/pkg/plugins/<plugin>` and `go get github.com/hashicorp/go-plugin`.
## Design the configuration ## Design the configuration
All plugins require a configuration. For example, the [Datadog plugin configuration](https://github.com/opencost/opencost-plugins/blob/main/datadog/datadogplugin/datadogconfig.go) takes in some information required to authenticate with the Datadog API. This configuration will be defined by a struct inside `<repo>/<plugin>/<plugin>plugin/`. All plugins require a configuration. For example, the [Datadog plugin configuration](https://github.com/opencost/opencost-plugins/blob/main/pkg/plugins/datadog/datadogplugin/datadogconfig.go) takes in some information required to authenticate with the Datadog API. This configuration will be defined by a struct inside `<repo>/pkg/plugins/<plugin>/<plugin>plugin/`.
## Implement the plugin ## Implement the plugin
@ -51,13 +51,13 @@ Once the configuration is designed, it's time to write the plugin. Within `<repo
- Optionally (but recommended), implement your response data such that the custom cost extended attributes are utilized as much as possible. - Optionally (but recommended), implement your response data such that the custom cost extended attributes are utilized as much as possible.
- [Finally](https://github.com/opencost/opencost-plugins/blob/00809062196b79ce354a5cdafaba1d6ed3f132f9/datadog/cmd/main/main.go#L87), we return the retrieved cost data. - [Finally](https://github.com/opencost/opencost-plugins/blob/00809062196b79ce354a5cdafaba1d6ed3f132f9/datadog/cmd/main/main.go#L87), we return the retrieved cost data.
- Implement the `main` function: - Implement the `main` function:
- Find the config file ([Datadog reference](https://github.com/opencost/opencost-plugins/blob/main/datadog/cmd/main/main.go#L92)). - Find the config file ([Datadog reference](https://github.com/opencost/opencost-plugins/blob/main/pkg/plugins/datadog/cmd/main/main.go#L92)).
- Load the config file ([Datadog reference](https://github.com/opencost/opencost-plugins/blob/main/datadog/cmd/main/main.go#L97)). - Load the config file ([Datadog reference](https://github.com/opencost/opencost-plugins/blob/main/pkg/plugins/datadog/cmd/main/main.go#L97)).
- Instantiate the plugin source ([Datadog reference](https://github.com/opencost/opencost-plugins/blob/00809062196b79ce354a5cdafaba1d6ed3f132f9/datadog/cmd/main/main.go#L104-L106)). - Instantiate the plugin source ([Datadog reference](https://github.com/opencost/opencost-plugins/blob/00809062196b79ce354a5cdafaba1d6ed3f132f9/datadog/cmd/main/main.go#L104-L106)).
- Serve the plugin for consumption by OpenCost ([Datadog reference](https://github.com/opencost/opencost-plugins/blob/00809062196b79ce354a5cdafaba1d6ed3f132f9/datadog/cmd/main/main.go#L110-L118)). - Serve the plugin for consumption by OpenCost ([Datadog reference](https://github.com/opencost/opencost-plugins/blob/00809062196b79ce354a5cdafaba1d6ed3f132f9/datadog/cmd/main/main.go#L110-L118)).
## Implement tests (highly recommended) ## Implement tests (highly recommended)
Write some unit tests to validate the functionality of your new plugin. See the [Datadog unit tests](https://github.com/opencost/opencost-plugins/blob/main/datadog/tests/datadog_test.go) for reference. Write some unit tests to validate the functionality of your new plugin. See the [Datadog unit tests](https://github.com/opencost/opencost-plugins/blob/main/pkg/plugins/datadog/tests/datadog_test.go) for reference.
## Submit it! ## Submit it!
Now that your plugin is implemented and tested, all that's left is to get it submitted for review. Create a PR based off your branch and submit it, and an OpenCost developer will review it for you. Now that your plugin is implemented and tested, all that's left is to get it submitted for review. Create a PR based off your branch and submit it, and an OpenCost developer will review it for you.
@ -66,3 +66,21 @@ Now that your plugin is implemented and tested, all that's left is to get it sub
- OpenCost stores the plugin responses in an in-memory repository, which necessitates that OpenCost queries the plugins again for cost data upon pod restart. - OpenCost stores the plugin responses in an in-memory repository, which necessitates that OpenCost queries the plugins again for cost data upon pod restart.
- Many cost sources have API rate limits, such as Datadog. As such, a rate limiter may be necessary. - Many cost sources have API rate limits, such as Datadog. As such, a rate limiter may be necessary.
- If you want a plugin embedded in your OpenCost image, you will have to build the image yourself. - If you want a plugin embedded in your OpenCost image, you will have to build the image yourself.
## Contributors
Thanks to all the individuals who have given their time and effort towards creating and maintaining these plugins:
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://github.com/sajit"><img src="https://avatars.githubusercontent.com/u/675316?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sajit Mathew Kunnumkal</b></sub></a><br /><a href="https://github.com/opencost/opencost-plugins/commits?author=sajit" title="Code">💻</a><br /><sub><b>MongoDB Atlas Plugin</b></sub></a><br /</td>
</tr>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->

View File

@ -1,4 +1,5 @@
# this manifest contains the name of every currently implemented plugin. it can be pulled via https://github.com/opencost/opencost-plugins/raw/main/manifest to get an up to date list of current plugins. # this manifest contains the name of every currently implemented plugin. it can be pulled via https://github.com/opencost/opencost-plugins/raw/main/manifest to get an up to date list of current plugins.
datadog datadog
mongodb-atlas
openai openai

View File

@ -4,20 +4,19 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net/http"
_nethttp "net/http" _nethttp "net/http"
"os" "os"
"regexp" "reflect"
"strconv"
"strings" "strings"
"time" "time"
"github.com/DataDog/datadog-api-client-go/v2/api/datadog" "github.com/DataDog/datadog-api-client-go/v2/api/datadog"
"github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2"
"github.com/agnivade/levenshtein"
"golang.org/x/time/rate" "golang.org/x/time/rate"
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
"github.com/DataDog/datadog-api-client-go/v2/api/datadogV1"
"github.com/hashicorp/go-plugin" "github.com/hashicorp/go-plugin"
commonconfig "github.com/opencost/opencost-plugins/pkg/common/config" commonconfig "github.com/opencost/opencost-plugins/pkg/common/config"
datadogplugin "github.com/opencost/opencost-plugins/pkg/plugins/datadog/datadogplugin" datadogplugin "github.com/opencost/opencost-plugins/pkg/plugins/datadog/datadogplugin"
@ -44,6 +43,7 @@ var handshakeConfig = plugin.HandshakeConfig{
type DatadogCostSource struct { type DatadogCostSource struct {
ddCtx context.Context ddCtx context.Context
usageApi *datadogV2.UsageMeteringApi usageApi *datadogV2.UsageMeteringApi
v1UsageApi *datadogV1.UsageMeteringApi
rateLimiter *rate.Limiter rateLimiter *rate.Limiter
} }
@ -60,20 +60,19 @@ func (d *DatadogCostSource) GetCustomCosts(req *pb.CustomCostRequest) []*pb.Cust
return results return results
} }
// Call the function to scrape prices
listPricing, err := scrapeDatadogPrices(url)
if err != nil {
log.Errorf("error getting dd pricing: %v", err)
errResp := pb.CustomCostResponse{
Errors: []string{fmt.Sprintf("error getting dd pricing: %v", err)},
}
results = append(results, &errResp)
return results
} else {
log.Debugf("got list pricing: %v", listPricing.Details)
}
for _, target := range targets { for _, target := range targets {
// Call the function to scrape prices
unitPricing, err := d.GetDDUnitPrices(target.Start().UTC())
if err != nil {
log.Errorf("error getting dd pricing: %v", err)
errResp := pb.CustomCostResponse{
Errors: []string{fmt.Sprintf("error getting dd pricing: %v", err)},
}
results = append(results, &errResp)
return results
} else {
log.Debugf("got unit pricing: %v", unitPricing)
}
// DataDog gets mad if we ask them to tell the future // DataDog gets mad if we ask them to tell the future
if target.Start().After(time.Now().UTC()) { if target.Start().After(time.Now().UTC()) {
log.Debugf("skipping future window %v", target) log.Debugf("skipping future window %v", target)
@ -81,7 +80,7 @@ func (d *DatadogCostSource) GetCustomCosts(req *pb.CustomCostRequest) []*pb.Cust
} }
log.Debugf("fetching DD costs for window %v", target) log.Debugf("fetching DD costs for window %v", target)
result := d.getDDCostsForWindow(target, listPricing) result := d.getDDCostsForWindow(target, unitPricing)
results = append(results, result) results = append(results, result)
} }
@ -105,7 +104,7 @@ func main() {
ddCostSrc := DatadogCostSource{ ddCostSrc := DatadogCostSource{
rateLimiter: rateLimiter, rateLimiter: rateLimiter,
} }
ddCostSrc.ddCtx, ddCostSrc.usageApi = getDatadogClients(*ddConfig) ddCostSrc.ddCtx, ddCostSrc.usageApi, ddCostSrc.v1UsageApi = getDatadogClients(*ddConfig)
// pluginMap is the map of plugins we can dispense. // pluginMap is the map of plugins we can dispense.
var pluginMap = map[string]plugin.Plugin{ var pluginMap = map[string]plugin.Plugin{
@ -132,7 +131,7 @@ func boilerplateDDCustomCost(win opencost.Window) pb.CustomCostResponse {
Costs: []*pb.CustomCost{}, Costs: []*pb.CustomCost{},
} }
} }
func (d *DatadogCostSource) getDDCostsForWindow(window opencost.Window, listPricing *datadogplugin.PricingInformation) *pb.CustomCostResponse { func (d *DatadogCostSource) getDDCostsForWindow(window opencost.Window, listPricing map[string]billableCost) *pb.CustomCostResponse {
ccResp := boilerplateDDCustomCost(window) ccResp := boilerplateDDCustomCost(window)
costs := map[string]*pb.CustomCost{} costs := map[string]*pb.CustomCost{}
nextPageId := "init" nextPageId := "init"
@ -196,34 +195,42 @@ func (d *DatadogCostSource) getDDCostsForWindow(window opencost.Window, listPric
continue continue
} }
desc, usageUnit, pricing, currency := getListingInfo(window, *resp.Data[index].Attributes.ProductFamily, *resp.Data[index].Attributes.Measurements[indexMeas].UsageType, listPricing) matched, pricing := matchUsageToPricing(*resp.Data[index].Attributes.Measurements[indexMeas].UsageType, listPricing)
ccResp.Currency = currency log.Infof("matched %s to %s", *resp.Data[index].Attributes.Measurements[indexMeas].UsageType, matched)
provId := *resp.Data[index].Attributes.PublicId + "/" + *resp.Data[index].Attributes.Measurements[indexMeas].UsageType provId := *resp.Data[index].Attributes.PublicId + "/" + *resp.Data[index].Attributes.Measurements[indexMeas].UsageType
if cost, found := costs[provId]; found { if matched == "" {
// we already have this cost type for the window, so just update the usages and costs log.Infof("no pricing found for %s", *resp.Data[index].Attributes.Measurements[indexMeas].UsageType)
cost.UsageQuantity += usageQty continue
cost.ListCost += usageQty * pricing }
billedCost := float32(pricing.Cost) * usageQty
if _, found := costs[provId]; found {
// we have already encountered this cost type for this window, so add to the existing cost entry
costs[provId].UsageQuantity += usageQty
costs[provId].BilledCost += billedCost
} else { } else {
// we have not encountered this cost type for this window yet, so create a new cost entry // we have not encountered this cost type for this window yet, so create a new cost entry
cost := pb.CustomCost{ cost := pb.CustomCost{
Zone: *resp.Data[index].Attributes.Region, Zone: *resp.Data[index].Attributes.Region,
AccountName: *resp.Data[index].Attributes.OrgName, AccountName: *resp.Data[index].Attributes.OrgName,
ChargeCategory: "usage", ChargeCategory: "usage",
Description: desc, Description: "nil",
ResourceName: *resp.Data[index].Attributes.Measurements[indexMeas].UsageType, ResourceName: *resp.Data[index].Attributes.Measurements[indexMeas].UsageType,
ResourceType: *resp.Data[index].Attributes.ProductFamily, ResourceType: *resp.Data[index].Attributes.ProductFamily,
Id: *resp.Data[index].Id, Id: *resp.Data[index].Id,
ProviderId: provId, ProviderId: provId,
Labels: map[string]string{}, Labels: map[string]string{},
ListCost: usageQty * pricing, ListCost: 0,
ListUnitPrice: pricing, ListUnitPrice: 0,
BilledCost: billedCost,
UsageQuantity: usageQty, UsageQuantity: usageQty,
UsageUnit: usageUnit, UsageUnit: pricing.unit,
ExtendedAttributes: nil, ExtendedAttributes: nil,
} }
costs[provId] = &cost costs[provId] = &cost
} }
} }
} }
if resp.Meta != nil && resp.Meta.Pagination != nil && resp.Meta.Pagination.NextRecordId.IsSet() { if resp.Meta != nil && resp.Meta.Pagination != nil && resp.Meta.Pagination.NextRecordId.IsSet() {
@ -243,92 +250,73 @@ func (d *DatadogCostSource) getDDCostsForWindow(window opencost.Window, listPric
// this post processing stage de-duplicates those usages and costs // this post processing stage de-duplicates those usages and costs
postProcess(&ccResp) postProcess(&ccResp)
// query from the first of the window's month until the window end's day so that we can properly adjust for the
// cumulative nature of the response
startDate := time.Date(window.Start().UTC().Year(), window.Start().UTC().Month(), 1, 0, 0, 0, 0, time.UTC)
endDate := time.Date(window.End().UTC().Year(), window.End().UTC().Month(), window.End().UTC().Day(), 0, 0, 0, 0, time.UTC)
view := "sub-org"
params := datadogV2.NewGetEstimatedCostByOrgOptionalParameters()
params.StartDate = &startDate
params.EndDate = &endDate
params.View = &view
resp, r, err := d.usageApi.GetEstimatedCostByOrg(d.ddCtx, *params)
if err != nil {
log.Errorf("Error when calling `UsageMeteringApi.GetEstimatedCostByOrg`: %v\n", err)
log.Errorf("Full HTTP response: %v\n", r)
ccResp.Errors = append(ccResp.Errors, err.Error())
}
previousChargeCosts := make(map[string]float32)
// estimated costs from datadog are per-day, so we scale in the event that we want hourly costs
var costFactor float32
switch window.Duration().Hours() {
case 24:
costFactor = 1.0
case 1:
costFactor = 1.0 / 24.0
default:
err = fmt.Errorf("unsupported window duration: %v hours", window.Duration().Hours())
log.Errorf("%v\n", err)
ccResp.Errors = append(ccResp.Errors, err.Error())
return &ccResp
}
costs = map[string]*pb.CustomCost{}
for _, costResp := range resp.Data {
attributes := costResp.Attributes
for _, charge := range attributes.Charges {
chargeCost := float32(*charge.Cost)
// we only care about non-zero totals. by filtering out non-totals, we avoid duplicate costs from the
// datadog response
if (chargeCost == 0) || (*charge.ChargeType != "total") {
continue
}
// adjust the charge cost, as the charges are cumulative throughout the response
adjustedChargeCost := chargeCost
if _, ok := previousChargeCosts[*charge.ProductName]; ok {
adjustedChargeCost -= previousChargeCosts[*charge.ProductName]
}
previousChargeCosts[*charge.ProductName] = chargeCost
adjustedChargeCost *= costFactor
if attributes.Date.Day() != window.Start().Day() {
continue
}
provId := *attributes.PublicId + "/" + *charge.ProductName
if cost, found := costs[provId]; found {
// we already have this cost type for the window, so just update the billed cost
cost.BilledCost += adjustedChargeCost
} else {
// we have not encountered this cost type for this window yet, so create a new cost entry
cost := pb.CustomCost{
Zone: *attributes.Region,
AccountName: *attributes.OrgName,
ChargeCategory: "billing",
ResourceName: *charge.ProductName,
Id: *costResp.Id,
ProviderId: provId,
Labels: map[string]string{},
BilledCost: adjustedChargeCost,
ExtendedAttributes: nil,
}
costs[provId] = &cost
}
}
}
for _, cost := range costs {
ccResp.Costs = append(ccResp.Costs, cost)
}
return &ccResp return &ccResp
} }
func matchUsageToPricing(usageType string, pricing map[string]billableCost) (string, *billableCost) {
// for the usage, remove _count from the end of the usage type
usageType = strings.TrimSuffix(usageType, "_count")
// not specific enough to match on
if usageType == "host" {
return "", nil
}
// if the usage type is in the pricing map, use that
if _, found := pricing[usageType]; found {
entry := pricing[usageType]
return usageType, &entry
}
// break up the usage on _
tokens := strings.Split(usageType, "_")
// find the first pricing key that contains all tokens
for key, price := range pricing {
matchesAll := true
for _, token := range tokens {
if !strings.Contains(key, token) {
matchesAll = false
break
}
}
if matchesAll {
return key, &price
}
}
// try replacing agent with infra and checking that
agentAsInfra := strings.ReplaceAll(usageType, "agent", "infra")
tokens = strings.Split(agentAsInfra, "_")
// find the first pricing key that contains all tokens
for key, price := range pricing {
matchesAll := true
for _, token := range tokens {
if !strings.Contains(key, token) {
matchesAll = false
break
}
}
if matchesAll {
return key, &price
}
}
// if still no pricing key is found, compute the levenshtein distance between the usage type and the pricing key
// and use the one with the smallest distance
smallestDist := 4000000000
var closestKey string
for key := range pricing {
distance := levenshtein.ComputeDistance(usageType, key)
if distance < smallestDist {
smallestDist = distance
closestKey = key
}
}
// remember the pricing keys we have already matched. if we have already matched a pricing key, don't match it again
entry := pricing[closestKey]
return closestKey, &entry
}
func postProcess(ccResp *pb.CustomCostResponse) { func postProcess(ccResp *pb.CustomCostResponse) {
if ccResp == nil { if ccResp == nil {
return return
@ -338,11 +326,11 @@ func postProcess(ccResp *pb.CustomCostResponse) {
ccResp.Costs = processLogUsage(ccResp.Costs) ccResp.Costs = processLogUsage(ccResp.Costs)
// removes any items that have 0 usage, either because of post processing or otherwise
ccResp.Costs = removeZeroUsages(ccResp.Costs)
// DBM queries have 200 * number of hosts included. We need to adjust the costs to reflect this // DBM queries have 200 * number of hosts included. We need to adjust the costs to reflect this
ccResp.Costs = adjustDBMQueries(ccResp.Costs) ccResp.Costs = adjustDBMQueries(ccResp.Costs)
// removes any items that have 0 usage, either because of post processing or otherwise
ccResp.Costs = removeZeroUsages(ccResp.Costs)
} }
// as per https://www.datadoghq.com/pricing/?product=database-monitoring#database-monitoring-can-i-still-use-dbm-if-i-have-additional-normalized-queries-past-the-a-hrefpricingallotmentsallotteda-amount // as per https://www.datadoghq.com/pricing/?product=database-monitoring#database-monitoring-can-i-still-use-dbm-if-i-have-additional-normalized-queries-past-the-a-hrefpricingallotmentsallotteda-amount
@ -383,75 +371,36 @@ func adjustDBMQueries(costs []*pb.CustomCost) []*pb.CustomCost {
return costs return costs
} }
// removes any items that have 0 usage, either because of post processing or otherwise // removes any items that have 0 usage or cost, either because of post processing or otherwise
func removeZeroUsages(costs []*pb.CustomCost) []*pb.CustomCost { func removeZeroUsages(costs []*pb.CustomCost) []*pb.CustomCost {
log.Tracef("POST -costs length before post processing: %d", len(costs))
for index := 0; index < len(costs); index++ { for index := 0; index < len(costs); index++ {
if costs[index].UsageQuantity < 0.001 { log.Tracef("POST - looking at cost %s with usage %f", costs[index].ResourceName, costs[index].UsageQuantity)
log.Debugf("removing cost %s because it has 0 usage", costs[index].ProviderId) if costs[index].UsageQuantity < 0.001 && costs[index].ListCost == 0.0 && costs[index].BilledCost == 0.0 {
if costs[index].ResourceName == "dbm_queries_count" {
log.Tracef("leaving dbm queries cost in place")
continue
}
log.Tracef("POST -removing cost %s because it has 0 usage", costs[index].ProviderId)
costs = append(costs[:index], costs[index+1:]...) costs = append(costs[:index], costs[index+1:]...)
index = 0 log.Tracef("POST - costs is now %d", len(costs))
index = -1
} }
} }
log.Tracef("POST -costs length after post processing: %d", len(costs))
return costs return costs
} }
func processInfraHosts(costs []*pb.CustomCost) []*pb.CustomCost { func processInfraHosts(costs []*pb.CustomCost) []*pb.CustomCost {
// remove the container_count_excl_agent item // remove the container_count item
// subtract the container_count_excl_agent from the container_count for index := 0; index < len(costs); index++ {
// re-add as a synthetic 'agent container' item
var cc *pb.CustomCost
for index := range costs {
if costs[index].ResourceName == "container_count" { if costs[index].ResourceName == "container_count" {
cc = costs[index]
costs = append(costs[:index], costs[index+1:]...) costs = append(costs[:index], costs[index+1:]...)
break index = 0
} }
} }
if cc != nil {
numAgents := float32(0.0)
for index := range costs {
if costs[index].ResourceName == "container_count_excl_agent" {
numAgents = float32(cc.UsageQuantity) - float32(costs[index].UsageQuantity)
break
}
}
cc.Description = "agent container"
cc.UsageQuantity = numAgents
cc.ResourceName = "agent_container"
cc.ListCost = numAgents * cc.ListUnitPrice
costs = append(costs, cc)
}
// remove the host_count item
// subtract the agent_cost_count from host_count item
// remaining gets put into a 'other hosts' item count
var hc *pb.CustomCost
for index := range costs {
if costs[index].ResourceName == "host_count" {
hc = costs[index]
costs = append(costs[:index], costs[index+1:]...)
break
}
}
if hc != nil {
otherHosts := float32(0.0)
for index := range costs {
if costs[index].ResourceName == "agent_host_count" {
otherHosts = float32(hc.UsageQuantity) - float32(costs[index].UsageQuantity)
break
}
}
hc.Description = "other hosts"
hc.UsageQuantity = otherHosts
hc.ResourceName = "other_hosts"
hc.ListCost = otherHosts * hc.ListUnitPrice
costs = append(costs, hc)
}
return costs return costs
} }
@ -499,117 +448,7 @@ func processLogUsage(costs []*pb.CustomCost) []*pb.CustomCost {
return costs return costs
} }
// the public pricing used in the pricing list doesn't always match the usage reports func getDatadogClients(config datadogplugin.DatadogConfig) (context.Context, *datadogV2.UsageMeteringApi, *datadogV1.UsageMeteringApi) {
// therefore, we maintain a list of aliases
var usageToPricingMap = map[string]string{
"timeseries": "custom_metrics",
"apm_uncategorized_host_count": "apm_hosts",
"apm_host_count_incl_usm": "apm_hosts",
"apm_azure_app_service_host_count": "apm_hosts",
"apm_devsecops_host_count": "apm_hosts",
"apm_host_count": "apm_hosts",
"opentelemetry_apm_host_count": "apm_hosts",
"apm_fargate_count": "apm_hosts",
"dbm_host_count": "dbm",
"dbm_queries_count": "dbm_queries",
"container_count": "containers",
"container_count_excl_agent": "containers",
"billable_ingested_bytes": "ingested_logs",
"ingested_events_bytes": "ingested_logs",
"logs_live_ingested_bytes": "ingested_logs",
"logs_rehydrated_ingested_bytes": "ingested_logs",
"indexed_events_count": "indexed_logs",
"logs_live_indexed_count": "indexed_logs",
"synthetics_api": "api_tests",
"synthetics_browser": "browser_checks",
"tasks_count": "fargate_tasks",
"rum": "rum_events",
"analyzed_logs": "security_logs",
"snmp": "snmp_device",
"invocations_sum": "serverless_inv",
}
var pricingMap = map[string]float64{
"custom_metrics": 100.0,
"indexed_logs": 1000000.0,
"ingested_logs": 1024.0 * 1024.0 * 1024.0 * 1024.0,
"api_tests": 10000.0,
"browser_checks": 1000.0,
"rum_events": 10000.0,
"security_logs": 1024.0 * 1024.0 * 1024.0 * 1024.0,
"serverless_inv": 1000000.0,
}
var rateFamilies = map[string]int{
"infra_hosts": 730.0,
"apm_hosts": 730.0,
"containers": 730.0,
"dbm": 730.0,
}
func getListingInfo(window opencost.Window, productfamily string, usageType string, listPricing *datadogplugin.PricingInformation) (description string, usageUnit string, pricing float32, currency string) {
pricingKey := ""
var found bool
// first, check if the usage type is mapped to a pricing key
if pricingKey, found = usageToPricingMap[usageType]; found {
log.Debugf("usage type %s was mapped to pricing key %s", usageType, pricingKey)
} else if pricingKey, found = usageToPricingMap[productfamily]; found {
// if it isn't then check if the family is mapped to a pricing key
log.Debugf("product family %s was mapped to pricing key %s", productfamily, pricingKey)
} else {
// if it isn't, then the family is the pricing key
pricingKey = productfamily
}
matchedPrice := false
// search through the pricing for the right key
for _, detail := range listPricing.Details {
if pricingKey == detail.Name {
matchedPrice = true
description = detail.DetailDescription
usageUnit = detail.Units
currency = detail.OneMonths.Currency
pricingFloat, err := strconv.ParseFloat(detail.OneMonths.Rate, 32)
if err != nil {
log.Errorf("error converting string to float for rate: %s", detail.OneMonths.Rate)
}
// if the family is a rate family, then the pricing is per hour
if hourlyPriceDenominator, found := rateFamilies[pricingKey]; found {
// adjust the pricing to fit the window duration
pricingPerHour := float32(pricingFloat) / float32(hourlyPriceDenominator)
pricingPerWindow := pricingPerHour
usageUnit = strings.TrimSuffix(usageUnit, "s")
usageUnit += " - hours"
pricing = pricingPerWindow
return
} else {
// if the family is a cumulative family, then the pricing is per unit
// check for a scale factor on the pricing
if scalefactor, found := pricingMap[pricingKey]; found {
pricing = float32(pricingFloat) / float32(scalefactor)
} else {
pricing = float32(pricingFloat)
}
return
}
}
}
if !matchedPrice {
log.Warnf("unable to find pricing for product %s/%s. going to set to 0 price", productfamily, usageType)
usageType = "PRICING UNAVAILABLE"
description = productfamily + " " + usageType
pricing = 0.0
currency = ""
}
// return the data from the usage entry
return
}
func getDatadogClients(config datadogplugin.DatadogConfig) (context.Context, *datadogV2.UsageMeteringApi) {
ddctx := datadog.NewDefaultContext(context.Background()) ddctx := datadog.NewDefaultContext(context.Background())
ddctx = context.WithValue( ddctx = context.WithValue(
ddctx, ddctx,
@ -631,7 +470,8 @@ func getDatadogClients(config datadogplugin.DatadogConfig) (context.Context, *da
configuration := datadog.NewConfiguration() configuration := datadog.NewConfiguration()
apiClient := datadog.NewAPIClient(configuration) apiClient := datadog.NewAPIClient(configuration)
usageAPI := datadogV2.NewUsageMeteringApi(apiClient) usageAPI := datadogV2.NewUsageMeteringApi(apiClient)
return ddctx, usageAPI v1UsageAPI := datadogV1.NewUsageMeteringApi(apiClient)
return ddctx, usageAPI, v1UsageAPI
} }
func getDatadogConfig(configFilePath string) (*datadogplugin.DatadogConfig, error) { func getDatadogConfig(configFilePath string) (*datadogplugin.DatadogConfig, error) {
@ -652,60 +492,143 @@ func getDatadogConfig(configFilePath string) (*datadogplugin.DatadogConfig, erro
return &result, nil return &result, nil
} }
func scrapeDatadogPrices(url string) (*datadogplugin.PricingInformation, error) { func (d *DatadogCostSource) GetDDUnitPrices(windowStart time.Time) (map[string]billableCost, error) {
maxTries := 5
var result *datadogplugin.PricingInformation
var errTry error
for try := 1; try <= maxTries; try++ {
var response *http.Response
// Send a GET request to the URL
response, errTry = http.Get(url)
if errTry != nil || response.StatusCode != http.StatusOK {
log.Errorf("failed to fetch the page: %v", errTry)
time.Sleep(30 * time.Second)
response.Body.Close()
continue
}
b, err := io.ReadAll(response.Body) // DD estimated costs can be delayed 72 hours
if err != nil { // so ensure we are going far enough back
errTry = err stableTimeframe := time.Now().UTC().Add(-3 * 24 * time.Hour)
response.Body.Close()
log.Errorf("failed to read pricing page body: %v", err)
time.Sleep(30 * time.Second)
continue
}
response.Body.Close()
res := datadogplugin.DatadogProJSON{}
r := regexp.MustCompile(`var productDetailData = \s*(.*?)\s*};`)
log.Tracef("got response: %s", string(b))
matches := r.FindAllStringSubmatch(string(b), -1)
if len(matches) != 1 {
errTry = err
log.Errorf("requires exactly 1 product detail data, got %d", len(matches))
time.Sleep(30 * time.Second)
continue
}
log.Tracef("matches[0][1]:" + matches[0][1]) targetMonth := time.Date(stableTimeframe.Year(), stableTimeframe.Month(), 1, 0, 0, 0, 0, time.UTC)
// add back in the closing curly brace that was used to pattern match targetMonthEnd := time.Date(stableTimeframe.Year(), stableTimeframe.Month()+1, 1, 0, 0, 0, 0, time.UTC)
err = json.Unmarshal([]byte(matches[0][1]+"}"), &res) // first, get the billable usage for the month
if err != nil { opts := datadogV1.GetUsageBillableSummaryOptionalParameters{
errTry = err Month: &targetMonth,
log.Errorf("failed to read pricing page body: %v", err) }
time.Sleep(30 * time.Second) var respBillableUsage datadogV1.UsageBillableSummaryResponse
continue var err error
} for try := 1; try <= 5; {
respBillableUsage, _, err = d.v1UsageApi.GetUsageBillableSummary(d.ddCtx, opts)
if errTry == nil { if err == nil {
result = &res.OfferData.PricingInformation
break break
} else {
if strings.Contains(err.Error(), "429") {
log.Errorf("rate limit reached, retrying...")
} else {
break
}
time.Sleep(30 * time.Second)
try++
} }
} }
if err != nil {
if errTry != nil { return nil, fmt.Errorf("error getting usage billable usage summary: %v", err)
return nil, fmt.Errorf("failed to fetch the page after %d tries: %w", maxTries, errTry)
} }
// then, get the estimated cost for the month
// the start date should be the beginning of the month
// the end date should be the end of the last month, or the stable time frame, depending on if we are in the first 3 days of the new month or not
endDateToUse := targetMonthEnd
if time.Now().Before(targetMonthEnd) {
endDateToUse = stableTimeframe
}
costOpts := datadogV2.GetEstimatedCostByOrgOptionalParameters{
StartDate: &targetMonth,
EndDate: &endDateToUse,
}
var respEstimatedCost datadogV2.CostByOrgResponse
for try := 1; try <= 5; {
respEstimatedCost, _, err = d.usageApi.GetEstimatedCostByOrg(d.ddCtx, costOpts)
if err == nil {
break
} else {
if strings.Contains(err.Error(), "429") {
log.Errorf("rate limit reached, retrying...")
} else {
break
}
time.Sleep(30 * time.Second)
try++
}
}
if err != nil {
return nil, fmt.Errorf("after calling `UsageMeteringApi.GetEstimatedCostByOrg` %d times, still getting error: %v", 5, err)
}
// now, we need to calculate the unit prices
// the unit price is the estimated cost divided by the billable usage
// we need to do this for each product family
costsByFamily := make(map[string]float64)
latestCosts := respEstimatedCost.Data[len(respEstimatedCost.Data)-1]
attrs := latestCosts.Attributes
for _, charge := range attrs.Charges {
if *charge.ChargeType != "total" {
continue
}
costsByFamily[*charge.ProductName] = float64(*charge.Cost)
}
result := make(map[string]billableCost)
for _, usage := range respBillableUsage.Usage {
log.Debugf("usage: %v", usage)
for productName, cost := range costsByFamily {
usageAmount, unit := GetAccountBillableUsage(productName, usage.Usage)
if usageAmount == 0 {
continue
}
// if the product family has 'hosts' in it, then the usage is per month
// so we need to adjust the cost to be per hour
isRated := false
if strings.Contains(productName, "host") {
isRated = true
cost /= float64(730)
}
result[productName] = billableCost{
ProductName: productName,
Cost: cost / float64(usageAmount),
isRated: isRated,
unit: unit,
}
}
}
return result, nil return result, nil
} }
type billableCost struct {
ProductName string
Cost float64
isRated bool
unit string
}
// CheckAccountBillableUsage checks if any AccountBillableUsage equals one.
func GetAccountBillableUsage(billingDimension string, o *datadogV1.UsageBillableSummaryKeys) (int64, string) {
v := reflect.ValueOf(o).Elem()
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
if field.Kind() == reflect.Ptr && !field.IsNil() {
usage := field.Interface().(*datadogV1.UsageBillableSummaryBody)
if len(usage.AdditionalProperties) > 0 {
if usage.AdditionalProperties["billing_dimension"] == billingDimension {
return *usage.AccountBillableUsage, *usage.UsageUnit
}
}
}
}
// if not in the reflected fields, check the AdditionalProperties
for name, usage := range o.AdditionalProperties {
if strings.Contains(name, billingDimension) {
untypedUsage := usage.(map[string]interface{})
untyped := untypedUsage["account_billable_usage"]
typed := int64(untyped.(float64))
return typed, untypedUsage["usage_unit"].(string)
}
}
log.Warnf("no AccountBillableUsage found for billing dimension %s", billingDimension)
return 0, ""
}

View File

@ -1,7 +1,6 @@
package main package main
import ( import (
"fmt"
"os" "os"
"testing" "testing"
"time" "time"
@ -15,17 +14,6 @@ import (
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
) )
func TestPricingFetch(t *testing.T) {
listPricing, err := scrapeDatadogPrices(url)
if err != nil {
t.Fatalf("failed to get pricing: %v", err)
}
fmt.Printf("got response: %v", listPricing)
if len(listPricing.Details) == 0 {
t.Fatalf("expected non zero pricing details")
}
}
func TestGetCustomCosts(t *testing.T) { func TestGetCustomCosts(t *testing.T) {
// read necessary env vars. If any are missing, log warning and skip test // read necessary env vars. If any are missing, log warning and skip test
ddSite := os.Getenv("DD_SITE") ddSite := os.Getenv("DD_SITE")
@ -61,10 +49,10 @@ func TestGetCustomCosts(t *testing.T) {
ddCostSrc := DatadogCostSource{ ddCostSrc := DatadogCostSource{
rateLimiter: rateLimiter, rateLimiter: rateLimiter,
} }
ddCostSrc.ddCtx, ddCostSrc.usageApi = getDatadogClients(config) ddCostSrc.ddCtx, ddCostSrc.usageApi, ddCostSrc.v1UsageApi = getDatadogClients(config)
windowStart := time.Date(2024, 10, 6, 0, 0, 0, 0, time.UTC) windowStart := time.Date(2024, 10, 16, 0, 0, 0, 0, time.UTC)
// query for qty 2 of 1 hour windows // query for qty 2 of 1 hour windows
windowEnd := time.Date(2024, 10, 7, 0, 0, 0, 0, time.UTC) windowEnd := time.Date(2024, 10, 17, 0, 0, 0, 0, time.UTC)
req := &pb.CustomCostRequest{ req := &pb.CustomCostRequest{
Start: timestamppb.New(windowStart), Start: timestamppb.New(windowStart),
@ -72,7 +60,7 @@ func TestGetCustomCosts(t *testing.T) {
Resolution: durationpb.New(timeutil.Day), Resolution: durationpb.New(timeutil.Day),
} }
log.SetLogLevel("debug") log.SetLogLevel("trace")
resp := ddCostSrc.GetCustomCosts(req) resp := ddCostSrc.GetCustomCosts(req)
if len(resp) == 0 { if len(resp) == 0 {

View File

@ -104,6 +104,7 @@ func validate(respDaily, respHourly []*pb.CustomCostResponse) bool {
} }
dbmCostsInRange := 0 dbmCostsInRange := 0
seenCosts := map[string]bool{}
//verify that the returned costs are non zero //verify that the returned costs are non zero
for _, resp := range respDaily { for _, resp := range respDaily {
if len(resp.Costs) == 0 && resp.Start.AsTime().After(time.Now().Truncate(24*time.Hour).Add(-1*time.Minute)) { if len(resp.Costs) == 0 && resp.Start.AsTime().After(time.Now().Truncate(24*time.Hour).Add(-1*time.Minute)) {
@ -112,12 +113,12 @@ func validate(respDaily, respHourly []*pb.CustomCostResponse) bool {
} }
var costSum float32 var costSum float32
for _, cost := range resp.Costs { for _, cost := range resp.Costs {
costSum += cost.GetListCost() costSum += cost.GetBilledCost()
seenCosts[cost.GetResourceName()] = true
if cost.GetListCost() == 0 { if cost.GetBilledCost() == 0 {
log.Debugf("got zero cost for %v", cost) log.Debugf("got zero cost for %v", cost)
} }
if cost.GetListCost() > 100 { if cost.GetBilledCost() > 100 {
log.Errorf("daily cost returned by plugin datadog for %v is greater than 100", cost) log.Errorf("daily cost returned by plugin datadog for %v is greater than 100", cost)
return false return false
} }
@ -126,7 +127,7 @@ func validate(respDaily, respHourly []*pb.CustomCostResponse) bool {
// range // range
if cost.GetResourceName() == "dbm_host_count" { if cost.GetResourceName() == "dbm_host_count" {
// filter out recent costs since those might not be full days worth // filter out recent costs since those might not be full days worth
if cost.GetListCost() > 2.5 && cost.GetListCost() < 3.0 { if cost.GetBilledCost() > 2.5 && cost.GetBilledCost() < 3.0 {
dbmCostsInRange++ dbmCostsInRange++
} }
} }
@ -143,41 +144,26 @@ func validate(respDaily, respHourly []*pb.CustomCostResponse) bool {
return false return false
} }
seenCosts := map[string]bool{}
for _, resp := range respHourly {
var costSum float32
for _, cost := range resp.Costs {
seenCosts[cost.GetResourceName()] = true
costSum += cost.GetListCost()
}
if costSum == 0 {
log.Errorf("hourly cost returned by plugin datadog is zero")
return false
}
}
expectedCosts := []string{ expectedCosts := []string{
"agent_host_count", "agent_host_count",
"logs_indexed_events_15_day_count", "logs_indexed_events_15_day_count",
"container_count_excl_agent", "container_count_excl_agent",
"agent_container",
"dbm_host_count", "dbm_host_count",
} }
for _, cost := range expectedCosts { for _, cost := range expectedCosts {
if !seenCosts[cost] { if !seenCosts[cost] {
log.Errorf("hourly cost %s not found in plugin datadog response", cost) log.Errorf("daily cost %s not found in plugin datadog response", cost)
return false return false
} }
} }
if len(seenCosts) != len(expectedCosts) { if len(seenCosts) != len(expectedCosts) {
log.Errorf("hourly costs returned by plugin datadog do not equal expected costs") log.Errorf("daily costs returned by plugin datadog do not equal expected costs")
log.Errorf("seen costs: %v", seenCosts) log.Errorf("seen costs: %v", seenCosts)
log.Errorf("expected costs: %v", expectedCosts) log.Errorf("expected costs: %v", expectedCosts)
log.Errorf("response: %v", respHourly) log.Errorf("response: %v", respDaily)
return false return false
} }
@ -190,18 +176,22 @@ func validate(respDaily, respHourly []*pb.CustomCostResponse) bool {
} }
seenCosts = map[string]bool{} seenCosts = map[string]bool{}
sumCosts := float32(0.0)
for _, resp := range respHourly { for _, resp := range respHourly {
for _, cost := range resp.Costs { for _, cost := range resp.Costs {
seenCosts[cost.GetResourceName()] = true seenCosts[cost.GetResourceName()] = true
if cost.GetListCost() == 0 { sumCosts += cost.GetBilledCost()
log.Errorf("hourly cost returned by plugin datadog is zero") if cost.GetBilledCost() > 100 {
return false
}
if cost.GetListCost() > 100 {
log.Errorf("hourly cost returned by plugin datadog for %v is greater than 100", cost) log.Errorf("hourly cost returned by plugin datadog for %v is greater than 100", cost)
return false return false
} }
} }
}
if sumCosts == 0 {
log.Errorf("hourly costs returned by datadog plugin are zero")
return false
} }
for _, cost := range expectedCosts { for _, cost := range expectedCosts {

View File

@ -18,6 +18,7 @@ require (
require ( require (
github.com/DataDog/zstd v1.5.5 // indirect github.com/DataDog/zstd v1.5.5 // indirect
github.com/agnivade/levenshtein v1.2.0 // indirect
github.com/fatih/color v1.16.0 // indirect github.com/fatih/color v1.16.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/logr v1.4.1 // indirect

View File

@ -2,6 +2,8 @@ github.com/DataDog/datadog-api-client-go/v2 v2.23.0 h1:1ziqo+mhG8GSuxsxLVBNNe/SX
github.com/DataDog/datadog-api-client-go/v2 v2.23.0/go.mod h1:QKOu6vscsh87fMY1lHfLEmNSunyXImj8BUaUWJXOehc= github.com/DataDog/datadog-api-client-go/v2 v2.23.0/go.mod h1:QKOu6vscsh87fMY1lHfLEmNSunyXImj8BUaUWJXOehc=
github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY=
github.com/agnivade/levenshtein v1.2.0/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=

View File

@ -0,0 +1,264 @@
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/hashicorp/go-plugin"
"github.com/icholy/digest"
commonconfig "github.com/opencost/opencost-plugins/common/config"
atlasconfig "github.com/opencost/opencost-plugins/pkg/plugins/mongodb-atlas/config"
atlasplugin "github.com/opencost/opencost-plugins/pkg/plugins/mongodb-atlas/plugin"
"github.com/opencost/opencost/core/pkg/log"
"github.com/opencost/opencost/core/pkg/model/pb"
"github.com/opencost/opencost/core/pkg/opencost"
ocplugin "github.com/opencost/opencost/core/pkg/plugin"
"golang.org/x/time/rate"
"google.golang.org/protobuf/types/known/timestamppb"
"k8s.io/apimachinery/pkg/util/uuid"
)
// handshakeConfigs are used to just do a basic handshake between
// a plugin and host. If the handshake fails, a user friendly error is shown.
// This prevents users from executing bad plugins or executing a plugin
// directory. It is a UX feature, not a security feature.
var handshakeConfig = plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "PLUGIN_NAME",
MagicCookieValue: "mongodb-atlas",
}
const costExplorerPendingInvoicesURL = "https://cloud.mongodb.com/api/atlas/v2/orgs/%s/invoices/pending"
func main() {
log.Debug("Initializing Mongo plugin")
configFile, err := commonconfig.GetConfigFilePath()
if err != nil {
log.Fatalf("error opening config file: %v", err)
}
atlasConfig, err := atlasconfig.GetAtlasConfig(configFile)
if err != nil {
log.Fatalf("error building Atlas config: %v", err)
}
log.SetLogLevel(atlasConfig.LogLevel)
// as per https://www.mongodb.com/docs/atlas/api/atlas-admin-api-ref/,
// atlas admin APIs have a limit of 100 requests per minute
rateLimiter := rate.NewLimiter(1.1, 2)
atlasCostSrc := AtlasCostSource{
rateLimiter: rateLimiter,
orgID: atlasConfig.OrgID,
}
atlasCostSrc.atlasClient = getAtlasClient(*atlasConfig)
// pluginMap is the map of plugins we can dispense.
var pluginMap = map[string]plugin.Plugin{
"CustomCostSource": &ocplugin.CustomCostPlugin{Impl: &atlasCostSrc},
}
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: handshakeConfig,
Plugins: pluginMap,
GRPCServer: plugin.DefaultGRPCServer,
})
}
func getAtlasClient(atlasConfig atlasconfig.AtlasConfig) HTTPClient {
return &http.Client{
Transport: &digest.Transport{
Username: atlasConfig.PublicKey,
Password: atlasConfig.PrivateKey,
},
}
}
// Implementation of CustomCostSource
type AtlasCostSource struct {
orgID string
rateLimiter *rate.Limiter
atlasClient HTTPClient
}
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
func validateRequest(req *pb.CustomCostRequest) []string {
var errors []string
now := time.Now()
// 1. Check if resolution is less than a day
if req.Resolution.AsDuration() < 24*time.Hour {
var resolutionMessage = "Resolution should be at least one day."
log.Warnf(resolutionMessage)
errors = append(errors, resolutionMessage)
}
// Get the start of the current month
currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
// 2. Check if start time is before the start of the current month
if req.Start.AsTime().Before(currentMonthStart) {
var startDateMessage = "Start date cannot be before the current month. Historical costs not currently supported"
log.Warnf(startDateMessage)
errors = append(errors, startDateMessage)
}
// 3. Check if end time is before the start of the current month
if req.End.AsTime().Before(currentMonthStart) {
var endDateMessage = "End date cannot be before the current month. Historical costs not currently supported"
log.Warnf(endDateMessage)
errors = append(errors, endDateMessage)
}
return errors
}
func (a *AtlasCostSource) GetCustomCosts(req *pb.CustomCostRequest) []*pb.CustomCostResponse {
results := []*pb.CustomCostResponse{}
requestErrors := validateRequest(req)
if len(requestErrors) > 0 {
//return empty response
return results
}
targets, err := opencost.GetWindows(req.Start.AsTime(), req.End.AsTime(), req.Resolution.AsDuration())
if err != nil {
log.Errorf("error getting windows: %v", err)
errResp := pb.CustomCostResponse{
Errors: []string{fmt.Sprintf("error getting windows: %v", err)},
}
results = append(results, &errResp)
return results
}
lineItems, err := GetPendingInvoices(a.orgID, a.atlasClient)
if err != nil {
log.Errorf("Error fetching invoices: %v", err)
errResp := pb.CustomCostResponse{
Errors: []string{fmt.Sprintf("error fetching invoices: %v", err)},
}
results = append(results, &errResp)
return results
}
for _, target := range targets {
if target.Start().After(time.Now().UTC()) {
log.Debugf("skipping future window %v", target)
continue
}
log.Debugf("fetching atlas costs for window %v", target)
result := a.getAtlasCostsForWindow(&target, lineItems)
results = append(results, result)
}
return results
}
func filterLineItemsByWindow(win *opencost.Window, lineItems []atlasplugin.LineItem) []*pb.CustomCost {
var filteredItems []*pb.CustomCost
winStartUTC := win.Start().UTC()
winEndUTC := win.End().UTC()
log.Debugf("Item window %s %s", winStartUTC, winEndUTC)
// Iterate over each line item
for _, item := range lineItems {
// Parse StartDate and EndDate from strings to time.Time
startDate, err1 := time.Parse("2006-01-02T15:04:05Z07:00", item.StartDate) // Assuming date format is "2006-01-02T15:04:05Z07:00"
endDate, err2 := time.Parse("2006-01-02T15:04:05Z07:00", item.EndDate) // Same format assumption
if err1 != nil || err2 != nil {
// If parsing fails, skip this item
if err1 != nil {
log.Warnf("%s", err1)
}
if err2 != nil {
log.Warnf("%s", err2)
}
continue
}
customCost := &pb.CustomCost{
AccountName: item.GroupName,
ChargeCategory: "Usage",
Description: fmt.Sprintf("Usage for %s", item.SKU),
ResourceName: item.SKU,
Id: string(uuid.NewUUID()),
ProviderId: fmt.Sprintf("%s/%s/%s", item.GroupId, item.ClusterName, item.SKU),
BilledCost: float32(item.TotalPriceCents) / 100.0,
ListCost: item.Quantity * item.UnitPriceDollars,
ListUnitPrice: item.UnitPriceDollars,
UsageQuantity: item.Quantity,
UsageUnit: item.Unit,
}
log.Debugf("Line Item %s %s", startDate.UTC(), endDate.UTC())
// Check if the item's StartDate >= win.start and EndDate <= win.end
if (startDate.UTC().After(winStartUTC) || startDate.UTC().Equal(winStartUTC)) &&
(endDate.UTC().Before(winEndUTC) || endDate.UTC().Equal(winEndUTC)) {
// // Append the customCost pointer to the slice
filteredItems = append(filteredItems, customCost)
}
}
return filteredItems
}
func (a *AtlasCostSource) getAtlasCostsForWindow(win *opencost.Window, lineItems []atlasplugin.LineItem) *pb.CustomCostResponse {
//filter responses between the win start and win end dates
costsInWindow := filterLineItemsByWindow(win, lineItems)
resp := pb.CustomCostResponse{
Metadata: map[string]string{"api_client_version": "v1"},
CostSource: "data_storage",
Domain: "mongodb-atlas",
Version: "v1",
Currency: "USD",
Start: timestamppb.New(*win.Start()),
End: timestamppb.New(*win.End()),
Errors: []string{},
Costs: costsInWindow,
}
return &resp
}
func GetPendingInvoices(org string, client HTTPClient) ([]atlasplugin.LineItem, error) {
request, _ := http.NewRequest("GET", fmt.Sprintf(costExplorerPendingInvoicesURL, org), nil)
request.Header.Set("Accept", "application/vnd.atlas.2023-01-01+json")
request.Header.Set("Content-Type", "application/vnd.atlas.2023-01-01+json")
response, error := client.Do(request)
if error != nil {
msg := fmt.Sprintf("getPending Invoices: error from server: %v", error)
log.Errorf(msg)
return nil, fmt.Errorf(msg)
}
defer response.Body.Close()
body, _ := io.ReadAll(response.Body)
log.Debugf("response Body: %s", string(body))
var pendingInvoicesResponse atlasplugin.PendingInvoice
respUnmarshalError := json.Unmarshal([]byte(body), &pendingInvoicesResponse)
if respUnmarshalError != nil {
msg := fmt.Sprintf("pendingInvoices: error unmarshalling response: %v", respUnmarshalError)
log.Errorf(msg)
return nil, fmt.Errorf(msg)
}
return pendingInvoicesResponse.LineItems, nil
}

View File

@ -0,0 +1,491 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"testing"
"time"
"github.com/icholy/digest"
atlasplugin "github.com/opencost/opencost-plugins/pkg/plugins/mongodb-atlas/plugin"
"github.com/opencost/opencost/core/pkg/model/pb"
"github.com/opencost/opencost/core/pkg/opencost"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/stretchr/testify/assert"
)
// Mock HTTPClient implementation
type MockHTTPClient struct {
DoFunc func(req *http.Request) (*http.Response, error)
}
// The MockHTTPClient's Do method uses a function defined at runtime to mock various responses
func (m *MockHTTPClient) Do(req *http.Request) (*http.Response, error) {
return m.DoFunc(req)
}
// FOR INTEGRATION TESTING PURPOSES ONLY
// expects 3 env variables to be set to work
// mapuk = public key for mongodb atlas
// maprk = private key for mongodb atlas
// maOrgId = orgId to be testsed
func TestMain(t *testing.T) {
publicKey := os.Getenv("mapuk")
privateKey := os.Getenv("maprk")
orgId := os.Getenv("maorgid")
if publicKey == "" || privateKey == "" || orgId == "" {
t.Skip("Skipping integration test.")
}
assert.NotNil(t, publicKey)
assert.NotNil(t, privateKey)
assert.NotNil(t, orgId)
client := &http.Client{
Transport: &digest.Transport{
Username: publicKey,
Password: privateKey,
},
}
atlasCostSource := AtlasCostSource{
orgID: "myOrg",
atlasClient: client,
}
// Define the start and end time for the window
now := time.Now()
currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
customCostRequest := pb.CustomCostRequest{
Start: timestamppb.New(currentMonthStart), // Start in current month
End: timestamppb.New(currentMonthStart.Add(24 * time.Hour)), // End in current month
Resolution: durationpb.New(24 * time.Hour), // 1 day resolution
}
resp := atlasCostSource.GetCustomCosts(&customCostRequest)
assert.NotEmpty(t, resp)
}
// tests for getCosts
func TestGetCostsPendingInvoices(t *testing.T) {
pendingInvoiceResponse := atlasplugin.PendingInvoice{
AmountBilledCents: 0,
AmountPaidCents: 0,
Created: "2024-10-01T02:00:26Z",
CreditsCents: 0,
EndDate: "2024-11-01T00:00:00Z",
Id: "66fb726b79b56205f9376437",
LineItems: []atlasplugin.LineItem{
{
ClusterName: "kubecost-mongo-dev-1",
Created: "2024-10-11T02:57:56Z",
EndDate: "2024-10-11T00:00:00Z",
GroupId: "66d7254246a21a41036ff33e",
GroupName: "Project 0",
Quantity: 6.035e-07,
SKU: "ATLAS_AWS_DATA_TRANSFER_DIFFERENT_REGION",
StartDate: "2024-10-10T00:00:00Z",
TotalPriceCents: 0,
Unit: "GB",
UnitPriceDollars: 0.02,
},
},
Links: []atlasplugin.Link{
{
Href: "https://cloud.mongodb.com/api/atlas/v2/orgs/66d7254246a21a41036ff2e9",
Rel: "self",
},
},
OrgId: "66d7254246a21a41036ff2e9",
SalesTaxCents: 0,
StartDate: "2024-10-01T00:00:00Z",
StartingBalanceCents: 0,
StatusName: "PENDING",
SubTotalCents: 0,
Updated: "2024-10-01T02:00:26Z",
}
mockResponseJson, _ := json.Marshal(pendingInvoiceResponse)
mockClient := &MockHTTPClient{
DoFunc: func(req *http.Request) (*http.Response, error) {
// Verify that the request method and URL are correct
if req.Method != http.MethodGet {
t.Errorf("expected GET request, got %s", req.Method)
}
expectedURL := fmt.Sprintf(costExplorerPendingInvoicesURL, "myOrg")
if req.URL.String() != expectedURL {
t.Errorf("expected URL %s, got %s", expectedURL, req.URL.String())
}
// Return a mock response with status 200 and mock JSON body
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBuffer(mockResponseJson)),
}, nil
},
}
lineItems, err := GetPendingInvoices("myOrg", mockClient)
assert.Nil(t, err)
assert.Equal(t, 1, len(lineItems))
for _, invoice := range pendingInvoiceResponse.LineItems {
assert.Equal(t, "kubecost-mongo-dev-1", invoice.ClusterName)
assert.Equal(t, "66d7254246a21a41036ff33e", invoice.GroupId)
assert.Equal(t, "Project 0", invoice.GroupName)
//TODO add more asserts on the fields
}
}
func TestGetCostErrorFromServer(t *testing.T) {
mockClient := &MockHTTPClient{
DoFunc: func(req *http.Request) (*http.Response, error) {
// Return a mock response with status 200 and mock JSON body
return nil, fmt.Errorf("mock error: failed to execute request")
},
}
costs, err := GetPendingInvoices("myOrg", mockClient)
assert.NotEmpty(t, err)
assert.Nil(t, costs)
}
func TestGetCostsBadMessage(t *testing.T) {
mockClient := &MockHTTPClient{
DoFunc: func(req *http.Request) (*http.Response, error) {
// Return a mock response with status 200 and mock JSON body
return &http.Response{
StatusCode: http.StatusInternalServerError,
Body: io.NopCloser(bytes.NewBufferString("No Jason No")),
}, nil
},
}
_, error := GetPendingInvoices("myOrd", mockClient)
assert.NotEmpty(t, error)
}
func TestGetAtlasCostsForWindow(t *testing.T) {
atlasCostSource := AtlasCostSource{
orgID: "myOrg",
}
// Define the start and end time for the window
day1 := time.Date(2024, time.October, 12, 0, 0, 0, 0, time.UTC) // Now
day2 := time.Date(2024, time.October, 13, 0, 0, 0, 0, time.UTC)
day3 := time.Date(2024, time.October, 14, 0, 0, 0, 0, time.UTC) // Now
lineItems := []atlasplugin.LineItem{
{
ClusterName: "kubecost-mongo-dev-1",
Created: "2024-10-11T02:57:56Z",
EndDate: day3.Format("2006-01-02T15:04:05Z07:00"),
GroupId: "66d7254246a21a41036ff33e",
GroupName: "Project 0",
Quantity: 6.035e-07,
SKU: "ATLAS_AWS_DATA_TRANSFER_DIFFERENT_REGION",
StartDate: day2.Format("2006-01-02T15:04:05Z07:00"),
TotalPriceCents: 0,
Unit: "GB",
UnitPriceDollars: 0.02,
},
{
ClusterName: "kubecost-mongo-dev-1",
Created: "2024-10-11T02:57:56Z",
EndDate: day2.Format("2006-01-02T15:04:05Z07:00"),
GroupId: "66d7254246a21a41036ff33e",
GroupName: "Project 0",
Quantity: 0.0555,
SKU: "ATLAS_AWS_DATA_TRANSFER_DIFFERENT_REGION",
StartDate: day1.Add(-24 * time.Hour).Format("2006-01-02T15:04:05Z07:00"),
TotalPriceCents: 0,
Unit: "GB",
UnitPriceDollars: 0.03,
},
}
// Create a new Window instance
window := opencost.NewWindow(&day2, &day3)
resp := atlasCostSource.getAtlasCostsForWindow(&window, lineItems)
assert.True(t, resp != nil)
assert.Equal(t, "data_storage", resp.CostSource)
assert.Equal(t, "mongodb-atlas", resp.Domain)
assert.Equal(t, "v1", resp.Version)
assert.Equal(t, "USD", resp.Currency)
assert.Equal(t, 1, len(resp.Costs))
}
func TestGetCosts(t *testing.T) {
pendingInvoiceResponse := atlasplugin.PendingInvoice{
AmountBilledCents: 0,
AmountPaidCents: 0,
Created: "2024-10-01T02:00:26Z",
CreditsCents: 0,
EndDate: "2024-11-01T00:00:00Z",
Id: "66fb726b79b56205f9376437",
LineItems: []atlasplugin.LineItem{},
Links: []atlasplugin.Link{
{
Href: "https://cloud.mongodb.com/api/atlas/v2/orgs/66d7254246a21a41036ff2e9",
Rel: "self",
},
},
OrgId: "66d7254246a21a41036ff2e9",
SalesTaxCents: 0,
StartDate: "2024-10-01T00:00:00Z",
StartingBalanceCents: 0,
StatusName: "PENDING",
SubTotalCents: 0,
Updated: "2024-10-01T02:00:26Z",
}
mockResponseJson, _ := json.Marshal(pendingInvoiceResponse)
mockClient := &MockHTTPClient{
DoFunc: func(req *http.Request) (*http.Response, error) {
//return costs
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBuffer(mockResponseJson)),
}, nil
},
}
atlasCostSource := AtlasCostSource{
orgID: "myOrg",
atlasClient: mockClient,
}
// Define the start and end time for the window
now := time.Now()
currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
customCostRequest := pb.CustomCostRequest{
Start: timestamppb.New(currentMonthStart), // Start in current month
End: timestamppb.New(currentMonthStart.Add(48 * time.Hour)), // End in current month
Resolution: durationpb.New(24 * time.Hour), // 1 day resolution
}
resp := atlasCostSource.GetCustomCosts(&customCostRequest)
assert.Equal(t, 2, len(resp))
assert.True(t, len(resp[0].Costs) == 0)
assert.True(t, len(resp[1].Costs) == 0)
}
func TestValidateRequest(t *testing.T) {
// Get current time and first day of the current month
now := time.Now()
currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
tests := []struct {
name string
req *pb.CustomCostRequest
expectedErrors []string
}{
{
name: "Valid request",
req: &pb.CustomCostRequest{
Start: timestamppb.New(currentMonthStart.Add(5 * time.Hour)), // Start in current month
End: timestamppb.New(currentMonthStart.Add(48 * time.Hour)), // End in current month
Resolution: durationpb.New(24 * time.Hour), // 1 day resolution
},
expectedErrors: []string{},
},
{
name: "Resolution less than a day",
req: &pb.CustomCostRequest{
Start: timestamppb.New(currentMonthStart.Add(5 * time.Hour)), // Start in current month
End: timestamppb.New(currentMonthStart.Add(48 * time.Hour)), // End in current month
Resolution: durationpb.New(12 * time.Hour), // 12 hours resolution (error)
},
expectedErrors: []string{"Resolution should be at least one day."},
},
{
name: "Start date before current month",
req: &pb.CustomCostRequest{
Start: timestamppb.New(currentMonthStart.Add(-48 * time.Hour)), // Start before current month (error)
End: timestamppb.New(currentMonthStart.Add(48 * time.Hour)), // End in current month
Resolution: durationpb.New(24 * time.Hour), // 1 day resolution
},
expectedErrors: []string{"Start date cannot be before the current month. Historical costs not currently supported"},
},
{
name: "End date before current month",
req: &pb.CustomCostRequest{
Start: timestamppb.New(currentMonthStart.Add(5 * time.Hour)), // Start in current month
End: timestamppb.New(currentMonthStart.Add(-48 * time.Hour)), // End before current month (error)
Resolution: durationpb.New(24 * time.Hour), // 1 day resolution
},
expectedErrors: []string{"End date cannot be before the current month. Historical costs not currently supported"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
errors := validateRequest(tt.req)
if len(errors) != len(tt.expectedErrors) {
t.Errorf("Expected %d errors, got %d", len(tt.expectedErrors), len(errors))
}
for i, err := range tt.expectedErrors {
if errors[i] != err {
t.Errorf("Expected error %q, got %q", err, errors[i])
}
}
})
}
}
func TestFilterInvoicesOnWindow(t *testing.T) {
// Setup test data
//day3.Format("2006-01-02T15:04:05Z07:00")
windowStart := time.Date(2024, time.October, 1, 0, 0, 0, 0, time.UTC)
windowEnd := time.Date(2024, time.October, 31, 0, 0, 0, 0, time.UTC)
window := opencost.NewWindow(&windowStart, &windowEnd)
lineItems := []atlasplugin.LineItem{
{StartDate: "2024-10-05T00:00:00Z", EndDate: "2024-10-10T00:00:00Z", UnitPriceDollars: 1.0, GroupName: "kubecost0",
SKU: "0", ClusterName: "cluster-0", GroupId: "A", TotalPriceCents: 45, Quantity: 2, Unit: "GB"}, // Within window
{StartDate: "2024-09-01T00:00:00Z", EndDate: "2024-09-30T00:00:00Z"}, // Before window
{StartDate: "2024-11-01T00:00:00Z", EndDate: "2024-11-10T00:00:00Z"}, // After window
{StartDate: "2024-10-01T00:00:00Z", EndDate: "2024-10-31T00:00:00Z", UnitPriceDollars: 5}, // Exactly matching the window
{StartDate: "2024-10-15T00:00:00Z", EndDate: "2024-10-20T00:00:00Z", UnitPriceDollars: 2.45}, // Fully within window
{StartDate: "2024-09-25T00:00:00Z", EndDate: "2024-10-13T00:00:00Z"}, // Partially in window
{StartDate: "2024-10-12T00:00:00Z", EndDate: "2024-11-01T00:00:00Z"}, // Partially in window
}
filteredItems := filterLineItemsByWindow(&window, lineItems)
// Verify results
assert.Equal(t, 3, len(filteredItems), "Expected 3 line items to be filtered")
//Check if the filtered items are the correct ones
expectedFilteredDates := []pb.CustomCost{
{
ListUnitPrice: 1.0,
},
{
ListUnitPrice: 5,
},
{
ListUnitPrice: 2.45,
},
}
for i, item := range filteredItems {
assert.Equal(t, expectedFilteredDates[i].ListUnitPrice, item.ListUnitPrice, "Unit price mismatch")
}
//assert mapping to CustomCost object
assert.Equal(t, lineItems[0].GroupName, filteredItems[0].AccountName, "accout name mismatch")
assert.Equal(t, "Usage", filteredItems[0].ChargeCategory)
assert.Equal(t, "Usage for 0", filteredItems[0].Description)
assert.Equal(t, "0", filteredItems[0].ResourceName)
assert.NotNil(t, filteredItems[0].Id)
assert.NotNil(t, filteredItems[0].ProviderId)
assert.Equal(t, "A/cluster-0/0", filteredItems[0].ProviderId)
assert.InDelta(t, float32(lineItems[0].TotalPriceCents)/100.0, filteredItems[0].BilledCost, 0.01)
assert.InDelta(t, filteredItems[0].ListCost, lineItems[0].Quantity*lineItems[0].UnitPriceDollars, 0.01)
assert.Equal(t, lineItems[0].Quantity, filteredItems[0].UsageQuantity)
assert.Equal(t, filteredItems[0].UsageUnit, lineItems[0].Unit)
}
func TestFilterInvoicesOnWindowBadResponse(t *testing.T) {
//setup a window between october 1st and october 31st 2024
windowStart := time.Date(2024, time.October, 1, 0, 0, 0, 0, time.UTC)
windowEnd := time.Date(2024, time.October, 31, 0, 0, 0, 0, time.UTC)
window := opencost.NewWindow(&windowStart, &windowEnd)
//lineItems has bad startdate and bad endDate
lineItems := []atlasplugin.LineItem{
{StartDate: "Bar", EndDate: "Foo", UnitPriceDollars: 1.0, GroupName: "kubecost0",
SKU: "0", ClusterName: "cluster-0", GroupId: "A", TotalPriceCents: 45, Quantity: 2, Unit: "GB"}, // Within window
// Partially in window
}
filteredItems := filterLineItemsByWindow(&window, lineItems)
assert.Equal(t, 0, len(filteredItems))
}
func TestBadWindow(t *testing.T) {
pendingInvoiceResponse := atlasplugin.PendingInvoice{}
mockResponseJson, _ := json.Marshal(pendingInvoiceResponse)
mockClient := &MockHTTPClient{
DoFunc: func(req *http.Request) (*http.Response, error) {
//return costs
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBuffer(mockResponseJson)),
}, nil
},
}
atlasCostSource := AtlasCostSource{
orgID: "myOrg",
atlasClient: mockClient,
}
now := time.Now()
currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
customCostRequest := pb.CustomCostRequest{
Start: timestamppb.New(currentMonthStart), // Start in current month
End: timestamppb.New(currentMonthStart.Add(5 * time.Hour)), // End in 5 hours
Resolution: durationpb.New(24 * time.Hour), // 1 day resolution
}
//this window should throw an error in the opencost.GetWindows method
resp := atlasCostSource.GetCustomCosts(&customCostRequest)
assert.True(t, len(resp[0].Errors) > 0)
}
func TestGetCostsReturnsErrorForPendingInvoices(t *testing.T) {
mockClient := &MockHTTPClient{
DoFunc: func(req *http.Request) (*http.Response, error) {
//return costs
return nil, fmt.Errorf("mock error: failed to execute request")
},
}
atlasCostSource := AtlasCostSource{
orgID: "myOrg",
atlasClient: mockClient,
}
// Define the start and end time for the window
now := time.Now()
currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
customCostRequest := pb.CustomCostRequest{
Start: timestamppb.New(currentMonthStart), // Start in current month
End: timestamppb.New(currentMonthStart.Add(48 * time.Hour)), // End in current month
Resolution: durationpb.New(24 * time.Hour), // 1 day resolution
}
resp := atlasCostSource.GetCustomCosts(&customCostRequest)
assert.True(t, len(resp[0].Errors) > 0)
}

View File

@ -0,0 +1,181 @@
package main
import (
"encoding/json"
"fmt"
"os"
"strings"
"github.com/hashicorp/go-multierror"
"github.com/opencost/opencost/core/pkg/log"
"github.com/opencost/opencost/core/pkg/model/pb"
"google.golang.org/protobuf/encoding/protojson"
)
// the validator is designed to allow plugin implementors to validate their plugin information
// as called by the central test harness.
// this avoids having to ask folks to re-implement the test harness over again for each plugin
// the integration test harness provides a path to a protobuf file for each window
// the validator can then read that in and further validate the response data
// using the domain knowledge of each plugin author
func main() {
// first arg is the path to the daily protobuf file
if len(os.Args) < 3 {
fmt.Println("Usage: validator <path-to-daily-protobuf-file> <path-to-hourly-protobuf-file>")
os.Exit(1)
}
dailyProtobufFilePath := os.Args[1]
// read in the protobuf file
data, err := os.ReadFile(dailyProtobufFilePath)
if err != nil {
fmt.Printf("Error reading daily protobuf file: %v\n", err)
os.Exit(1)
}
dailyCustomCostResponses, err := Unmarshal(data)
if err != nil {
fmt.Printf("Error unmarshalling daily protobuf data: %v\n", err)
os.Exit(1)
}
fmt.Printf("Successfully unmarshalled %d daily custom cost responses\n", len(dailyCustomCostResponses))
// second arg is the path to the hourly protobuf file
hourlyProtobufFilePath := os.Args[2]
data, err = os.ReadFile(hourlyProtobufFilePath)
if err != nil {
fmt.Printf("Error reading hourly protobuf file: %v\n", err)
os.Exit(1)
}
// read in the protobuf file
hourlyCustomCostResponses, err := Unmarshal(data)
if err != nil {
fmt.Printf("Error unmarshalling hourly protobuf data: %v\n", err)
os.Exit(1)
}
fmt.Printf("Successfully unmarshalled %d hourly custom cost responses\n", len(hourlyCustomCostResponses))
// validate the custom cost response data
isvalid := validate(dailyCustomCostResponses, hourlyCustomCostResponses)
if !isvalid {
os.Exit(1)
} else {
fmt.Println("Validation successful")
}
}
func validate(respDaily, respHourly []*pb.CustomCostResponse) bool {
if len(respDaily) == 0 {
log.Errorf("no daily response received from mongodb-atlas plugin")
return false
}
if len(respHourly) != 0 {
log.Errorf("mongo plugin does not support hourly costs")
return false
}
var multiErr error
// parse the response and look for errors
for _, resp := range respDaily {
if len(resp.Errors) > 0 {
multiErr = multierror.Append(multiErr, fmt.Errorf("errors occurred in daily response: %v", resp.Errors))
}
}
for _, resp := range respHourly {
if resp.Errors != nil {
multiErr = multierror.Append(multiErr, fmt.Errorf("errors occurred in hourly response: %v", resp.Errors))
}
}
// check if any errors occurred
if multiErr != nil {
log.Errorf("Errors occurred during plugin testing for mongodb-atlas: %v", multiErr)
return false
}
seenCosts := map[string]bool{}
nonZeroBilledCosts := 0
//verify that the returned costs are non zero
for _, resp := range respDaily {
for _, cost := range resp.Costs {
seenCosts[cost.GetResourceName()] = true
if !strings.Contains(cost.GetResourceName(), "FREE") && cost.GetListCost() == 0 {
log.Errorf("daily list cost returned by plugin mongodb-atlas is zero for cost: %v", cost)
return false
}
if cost.GetListCost() >= 0.01 && !strings.Contains(cost.GetResourceName(), "FREE") && cost.GetBilledCost() == 0 {
log.Errorf("daily billed cost returned by plugin mongodb-atlas is zero for cost: %v", cost)
return false
}
if cost.GetBilledCost() > 0 {
nonZeroBilledCosts++
}
}
}
if nonZeroBilledCosts == 0 {
log.Errorf("no non-zero billed costs returned by plugin mongodb-atlas")
return false
}
expectedCosts := []string{
"ATLAS_AWS_DATA_TRANSFER_DIFFERENT_REGION",
"ATLAS_AWS_DATA_TRANSFER_INTERNET",
"ATLAS_AWS_DATA_TRANSFER_SAME_REGION",
"ATLAS_AWS_INSTANCE_M10",
"ATLAS_NDS_AWS_PIT_RESTORE_STORAGE",
"ATLAS_NDS_AWS_PIT_RESTORE_STORAGE_FREE_TIER",
}
for _, cost := range expectedCosts {
if !seenCosts[cost] {
log.Errorf("hourly cost %s not found in plugin mongodb-atlas response", cost)
return false
}
}
if len(seenCosts) != len(expectedCosts) {
log.Errorf("hourly costs returned by plugin mongodb-atlas do not equal expected costs")
log.Errorf("seen costs: %v", seenCosts)
log.Errorf("expected costs: %v", expectedCosts)
log.Errorf("response: %v", respHourly)
return false
}
// verify the domain matches the plugin name
for _, resp := range respDaily {
if resp.Domain != "mongodb-atlas" {
log.Errorf("daily domain returned by plugin mongodb-atlas does not match plugin name")
return false
}
}
return true
}
func Unmarshal(data []byte) ([]*pb.CustomCostResponse, error) {
var raw []json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
protoResps := make([]*pb.CustomCostResponse, len(raw))
for i, r := range raw {
p := &pb.CustomCostResponse{}
if err := protojson.Unmarshal(r, p); err != nil {
return nil, err
}
protoResps[i] = p
}
return protoResps, nil
}

View File

@ -0,0 +1,32 @@
package config
import (
"encoding/json"
"fmt"
"os"
)
type AtlasConfig struct {
PublicKey string `json:"atlas_public_key"`
PrivateKey string `json:"atlas_private_key"`
OrgID string `json:"atlas_org_id"`
LogLevel string `json:"atlas_plugin_log_level"`
}
func GetAtlasConfig(configFilePath string) (*AtlasConfig, error) {
var result AtlasConfig
bytes, err := os.ReadFile(configFilePath)
if err != nil {
return nil, fmt.Errorf("error reading config file for Atlas config @ %s: %v", configFilePath, err)
}
err = json.Unmarshal(bytes, &result)
if err != nil {
return nil, fmt.Errorf("error marshaling json into Atlas config %v", err)
}
if result.LogLevel == "" {
result.LogLevel = "info"
}
return &result, nil
}

View File

@ -0,0 +1,77 @@
package config
import (
"fmt"
"os"
"testing"
)
// Unit tests for the GetAtlasConfig function
func TestGetAtlasConfig(t *testing.T) {
// Test: Valid configuration file
t.Run("Valid configuration file", func(t *testing.T) {
configFilePath := "test_valid_config.json"
// Create a temporary valid JSON file
validConfig := `{"atlas_plugin_log_level": "debug"}`
err := os.WriteFile(configFilePath, []byte(validConfig), 0644)
if err != nil {
t.Fatalf("failed to create temporary config file: %v", err)
}
defer os.Remove(configFilePath)
config, err := GetAtlasConfig(configFilePath)
if err != nil {
t.Fatalf("expected no error, but got: %v", err)
}
fmt.Println(config, configFilePath)
if config.LogLevel != "debug" {
t.Errorf("expected log level to be 'debug', but got: %s", config.LogLevel)
}
})
// Test: Invalid file path
t.Run("Invalid file path", func(t *testing.T) {
configFilePath := "invalid_path.json"
_, err := GetAtlasConfig(configFilePath)
if err == nil {
t.Errorf("expected an error, but got none")
}
})
// Test: Invalid JSON format
t.Run("Invalid JSON format", func(t *testing.T) {
configFilePath := "test_invalid_json.json"
// Create a temporary invalid JSON file
invalidConfig := `{"log_level": "debug"`
err := os.WriteFile(configFilePath, []byte(invalidConfig), 0644)
if err != nil {
t.Fatalf("failed to create temporary config file: %v", err)
}
defer os.Remove(configFilePath)
_, err = GetAtlasConfig(configFilePath)
if err == nil {
t.Errorf("expected an error, but got none")
}
})
// Test: Default log level when missing
t.Run("Default log level when missing", func(t *testing.T) {
configFilePath := "test_missing_log_level.json"
// Create a temporary JSON file without log_level
missingLogLevelConfig := `{}`
err := os.WriteFile(configFilePath, []byte(missingLogLevelConfig), 0644)
if err != nil {
t.Fatalf("failed to create temporary config file: %v", err)
}
defer os.Remove(configFilePath)
config, err := GetAtlasConfig(configFilePath)
if err != nil {
t.Fatalf("expected no error, but got: %v", err)
}
if config.LogLevel != "info" {
t.Errorf("expected log level to be 'info', but got: %s", config.LogLevel)
}
})
}

View File

@ -0,0 +1,67 @@
module github.com/opencost/opencost-plugins/pkg/plugins/mongodb-atlas
go 1.22.5
replace github.com/opencost/opencost-plugins/common => ../../common
require (
github.com/hashicorp/go-plugin v1.6.1
github.com/icholy/digest v0.1.23
github.com/opencost/opencost-plugins/common v0.0.0-00010101000000-000000000000
github.com/opencost/opencost/core v0.0.0-20240829194822-b82370afd830
github.com/stretchr/testify v1.9.0
golang.org/x/time v0.6.0
google.golang.org/protobuf v1.34.2
k8s.io/apimachinery v0.25.3
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/goccy/go-json v0.9.11 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pelletier/go-toml v1.9.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rs/zerolog v1.26.1 // indirect
github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.8.1 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/text v0.17.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect
google.golang.org/grpc v1.66.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.25.3 // indirect
k8s.io/klog/v2 v2.80.0 // indirect
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
)

View File

@ -0,0 +1,697 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-plugin v1.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOsVdwI=
github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/icholy/digest v0.1.23 h1:4hX2pIloP0aDx7RJW0JewhPPy3R8kU+vWKdxPsCCGtY=
github.com/icholy/digest v0.1.23/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
github.com/opencost/opencost/core v0.0.0-20240829194822-b82370afd830 h1:PDYQw0cygJ8ehn/AObpRVru4Cg718aGrDJQis4XfHWg=
github.com/opencost/opencost/core v0.0.0-20240829194822-b82370afd830/go.mod h1:c1he7ogYA3J/m2BNbWD5FDLRTpUwuG8CGIyOkDV+i+s=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44=
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
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/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 h1:QfTh0HpN6hlw6D3vu8DAwC8pBIwikq0AI1evdm+FksE=
golang.org/x/exp v0.0.0-20221031165847-c99f073a8326/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed h1:J6izYgfBXAI3xTKLgxzTmUltdYaLsuBxFCgDHWJ/eXg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c=
google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
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-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
k8s.io/api v0.25.3 h1:Q1v5UFfYe87vi5H7NU0p4RXC26PPMT8KOpr1TLQbCMQ=
k8s.io/api v0.25.3/go.mod h1:o42gKscFrEVjHdQnyRenACrMtbuJsVdP+WVjqejfzmI=
k8s.io/apimachinery v0.25.3 h1:7o9ium4uyUOM76t6aunP0nZuex7gDf8VGwkR5RcJnQc=
k8s.io/apimachinery v0.25.3/go.mod h1:jaF9C/iPNM1FuLl7Zuy5b9v+n35HGSh6AQ4HYRkCqwo=
k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
k8s.io/klog/v2 v2.80.0 h1:lyJt0TWMPaGoODa8B8bUuxgHS3W/m/bNr2cca3brA/g=
k8s.io/klog/v2 v2.80.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed h1:jAne/RjBTyawwAy0utX5eqigAwz/lQhTmy+Hr/Cpue4=
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k=
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

View File

@ -0,0 +1,66 @@
package plugin
type CreateCostExplorerQueryPayload struct {
Clusters []string `json:"clusters"`
EndDate string `json:"endDate"`
GroupBy string `json:"groupBy"`
IncludePartialMatches bool `json:"includePartialMatches"`
Organizations []string `json:"organizations"`
Projects []string `json:"projects"`
Services []string `json:"services"`
StartDate string `json:"startDate"`
}
type CreateCostExplorerQueryResponse struct {
Token string `json:"token"`
}
type Invoice struct {
InvoiceId string `json:"invoiceId"`
OrganizationId string `json:"organizationId"`
OrganizationName string `json:"organizationName"`
Service string `json:"service"`
UsageAmount float32 `json:"usageAmount"`
UsageDate string `json:"usageDate"`
//"invoiceId":"66d7254246a21a41036ff315","organizationId":"66d7254246a21a41036ff2e9","organizationName":"Kubecost","service":"Clusters","usageAmount":51.19,"usageDate":"2024-09-01"}
}
type CostResponse struct {
UsageDetails []Invoice `json:"usageDetails"`
}
type PendingInvoice struct {
AmountBilledCents int32 `json:"amountBilledCents"`
AmountPaidCents int32 `json:"amountPaidCents"`
Created string `json:"created"`
CreditsCents int32 `json:"creditCents"`
Id string `json:"id"`
EndDate string `json:"endDate"`
LineItems []LineItem `json:"lineItems"`
Links []Link `json:"links"`
OrgId string `json:"orgId"`
SalesTaxCents int32 `json:"salesTaxCents"`
StartDate string `json:"startDate"`
StartingBalanceCents int32 `json:"startingBalanceCents"`
StatusName string `json:"statusName"`
SubTotalCents int32 `json:"subtotalCents"`
Updated string `json:"updated"`
}
type Link struct {
Href string `json:"href"`
Rel string `json:"rel"`
}
type LineItem struct {
ClusterName string `json:"clusterName"`
Created string `json:"created"`
EndDate string `json:"endDate"`
GroupId string `json:"groupId"`
GroupName string `json:"groupName"`
Quantity float32 `json:"quantity"`
SKU string `json:"sku"`
StartDate string `json:"startDate"`
TotalPriceCents int32 `json:"totalPriceCents"`
Unit string `json:"unit"`
UnitPriceDollars float32 `json:"unitPriceDollars"`
}

View File

@ -219,7 +219,7 @@ func (d *OpenAICostSource) getOpenAIBilling(start time.Time, end time.Time) (*op
req, errReq = http.NewRequest("GET", openAIBillingURL, nil) req, errReq = http.NewRequest("GET", openAIBillingURL, nil)
if errReq != nil { if errReq != nil {
log.Warnf("error creating billing export request: %v", errReq) log.Warnf("error creating billing export request: %v", errReq)
log.Warnf("retrying request after 30s") log.Infof("retrying request after 30s")
time.Sleep(30 * time.Second) time.Sleep(30 * time.Second)
continue continue
} }
@ -246,7 +246,7 @@ func (d *OpenAICostSource) getOpenAIBilling(start time.Time, end time.Time) (*op
errReq = fmt.Errorf("received non-200 response for billing export request: %d", resp.StatusCode) errReq = fmt.Errorf("received non-200 response for billing export request: %d", resp.StatusCode)
log.Warnf("got non-200 response for billing export request: %d, body is: %s", resp.StatusCode, bodyString) log.Warnf("got non-200 response for billing export request: %d, body is: %s", resp.StatusCode, bodyString)
log.Warnf("retrying request after 30s") log.Infof("retrying request after 30s")
time.Sleep(30 * time.Second) time.Sleep(30 * time.Second)
continue continue
} else { } else {
@ -304,7 +304,7 @@ func (d *OpenAICostSource) getOpenAITokenUsages(targetTime time.Time) (*openaipl
resp, errReq = client.Do(req) resp, errReq = client.Do(req)
if errReq != nil { if errReq != nil {
log.Warnf("error doing token request: %v", errReq) log.Warnf("error doing token request: %v", errReq)
log.Warnf("retrying request after 30s") log.Infof("retrying request after 30s")
time.Sleep(30 * time.Second) time.Sleep(30 * time.Second)
continue continue
} }
@ -320,7 +320,7 @@ func (d *OpenAICostSource) getOpenAITokenUsages(targetTime time.Time) (*openaipl
bodyString = string(bodyBytes) bodyString = string(bodyBytes)
} }
log.Warnf("got non-200 response for token usage request: %d, body is: %s", resp.StatusCode, bodyString) log.Warnf("got non-200 response for token usage request: %d, body is: %s", resp.StatusCode, bodyString)
log.Warnf("retrying request after 30s") log.Infof("retrying request after 30s")
time.Sleep(30 * time.Second) time.Sleep(30 * time.Second)
continue continue
} else { } else {

View File

@ -106,7 +106,7 @@ func validate(respDaily, respHourly []*pb.CustomCostResponse) bool {
if cost.GetBilledCost() == 0 { if cost.GetBilledCost() == 0 {
log.Debugf("got zero cost for %v", cost) log.Debugf("got zero cost for %v", cost)
} }
if cost.GetBilledCost() > 1 { if cost.GetBilledCost() > 2 {
log.Errorf("daily cost returned by plugin openai for %v is greater than 1", cost) log.Errorf("daily cost returned by plugin openai for %v is greater than 1", cost)
return false return false
} }
@ -120,7 +120,6 @@ func validate(respDaily, respHourly []*pb.CustomCostResponse) bool {
expectedCosts := []string{ expectedCosts := []string{
"GPT-4o mini", "GPT-4o mini",
"GPT-4o", "GPT-4o",
"Other models",
} }
for _, cost := range expectedCosts { for _, cost := range expectedCosts {

View File

@ -41,6 +41,10 @@ func main() {
// for each plugin given via a flag // for each plugin given via a flag
for _, plugin := range plugins { for _, plugin := range plugins {
log.Infof("Testing plugin: %s", plugin) log.Infof("Testing plugin: %s", plugin)
plugin = strings.TrimSpace(plugin)
if plugin == "" {
continue
}
// write the config in PLUGIN_NAME_CONFIG out to a file // write the config in PLUGIN_NAME_CONFIG out to a file
envVarName := fmt.Sprintf("%s_CONFIG", strings.ReplaceAll(strings.ToUpper(plugin), "-", "_")) envVarName := fmt.Sprintf("%s_CONFIG", strings.ReplaceAll(strings.ToUpper(plugin), "-", "_"))
@ -71,9 +75,9 @@ func main() {
pluginPath := cwd + "/pkg/plugins/" + plugin pluginPath := cwd + "/pkg/plugins/" + plugin
respDaily := getResponse(pluginPath, file.Name(), windowStart, windowEnd, 24*time.Hour) respDaily := getResponse(pluginPath, file.Name(), windowStart, windowEnd, 24*time.Hour)
// request usage for 2 days ago in hourly increments // request usage for 3 days ago in hourly increments
windowStart = time.Now().AddDate(0, 0, -2).Truncate(24 * time.Hour) windowStart = time.Now().AddDate(0, 0, -4).Truncate(24 * time.Hour)
windowEnd = time.Now().AddDate(0, 0, -1).Truncate(24 * time.Hour) windowEnd = time.Now().AddDate(0, 0, -3).Truncate(24 * time.Hour)
// invoke plugin via harness // invoke plugin via harness
respHourly := getResponse(pluginPath, file.Name(), windowStart, windowEnd, 1*time.Hour) respHourly := getResponse(pluginPath, file.Name(), windowStart, windowEnd, 1*time.Hour)