Compare commits
65 Commits
Author | SHA1 | Date |
---|---|---|
|
d8eaf32582 | |
|
42fa6c2496 | |
|
4c5d648738 | |
|
b60c571ab9 | |
|
5ebd41de6d | |
|
39e7d175b0 | |
|
173d98518d | |
|
5b117bca5e | |
|
f04fa90300 | |
|
779ed10797 | |
|
5a22aa2b41 | |
|
0c42e1a5e8 | |
|
42d14e3330 | |
|
30e43a6750 | |
|
7acbbf3320 | |
|
868e81de4e | |
|
64f25647d6 | |
|
574b999176 | |
|
28efd4a124 | |
|
c71ddf00d2 | |
|
ca198ef6b0 | |
|
d9672eff99 | |
|
662919659d | |
|
8fdf92aa5b | |
|
0af7673a74 | |
|
55854928ee | |
|
6ec64ff14f | |
|
55d5862b15 | |
|
973f722775 | |
|
5c92c4e539 | |
|
ee2355a22c | |
|
edf9b3f6c0 | |
|
f97d15a50f | |
|
d9ed7d1aaa | |
|
391002b1b1 | |
|
34fe29409a | |
|
32b6aa4e3a | |
|
0a6b612457 | |
|
4783329c91 | |
|
da31a9b273 | |
|
a60946d46d | |
|
af32ba71be | |
|
62270a223d | |
|
7ce614fb87 | |
|
dcc2acb880 | |
|
bc6621459f | |
|
0c6cf39340 | |
|
56571088b8 | |
|
a50cd12908 | |
|
d7050248b5 | |
|
7649371bd8 | |
|
9e249b4e32 | |
|
72d294ba75 | |
|
ef54e1a991 | |
|
43f1bbaf0f | |
|
f57f03222b | |
|
0080906219 | |
|
172348d97e | |
|
b31600e1a5 | |
|
7d075df022 | |
|
98f428bc96 | |
|
eda3aa78a4 | |
|
9b2c5dc5a0 | |
|
cc20b79057 | |
|
e5ae4ea9e5 |
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
name: OpenCost Bug report
|
||||
name: OpenCost Plugins Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
|
@ -8,7 +8,7 @@ assignees: ''
|
|||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the OpenCost bug is. Please ensure this is an issue related to the OpenCost cost model, API, UI or specification. Public Kubecost bugs may be opened at https://github.com/kubecost/cost-analyzer-helm-chart/
|
||||
A clear and concise description of what the OpenCost Plugins bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
@ -23,8 +23,8 @@ A clear and concise description of what you expected to happen.
|
|||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Which version of OpenCost are you using?**
|
||||
This may be the Kubecost release.
|
||||
**Which version of OpenCost Plugins are you using?**
|
||||
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here. Kubernetes versions and which public clouds you are working with are especially important.
|
||||
Add any other context about the problem here.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
name: OpenCost Feature request
|
||||
name: OpenCost Plugins Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
|
|
|
@ -8,13 +8,10 @@
|
|||
*
|
||||
|
||||
## Does this PR address any GitHub or Zendesk issues?
|
||||
* Closes ...
|
||||
*
|
||||
|
||||
## How was this PR tested?
|
||||
*
|
||||
|
||||
## Does this PR require changes to documentation?
|
||||
*
|
||||
|
||||
## Have you labeled this PR and its corresponding Issue as "next release" if it should be part of the next OpenCost release? If not, why not?
|
||||
*
|
||||
*
|
|
@ -0,0 +1,66 @@
|
|||
name: Run Integration Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
schedule:
|
||||
- cron: "0 5 * * *"
|
||||
|
||||
jobs:
|
||||
integration-test-runner:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: oNaiPs/secrets-to-env-action@v1
|
||||
with:
|
||||
secrets: ${{ toJSON(secrets) }}
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
path: ./
|
||||
ref: main
|
||||
- name: Install just
|
||||
uses: extractions/setup-just@v2
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "stable"
|
||||
- name: prep workspace
|
||||
run: |
|
||||
just init-workspace
|
||||
- name: run integration tests
|
||||
run: |
|
||||
just integration-test-all-plugins
|
||||
|
||||
|
||||
notify-fail:
|
||||
needs: [integration-test-runner]
|
||||
runs-on: ubuntu-latest
|
||||
if: failure()
|
||||
steps:
|
||||
- name: Slack notify
|
||||
id: slack
|
||||
uses: slackapi/slack-github-action@v1
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"workflow": "${{github.workflow}}",
|
||||
"message": "Plugin Integration tests have failed! Please check the logs for more information."
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
|
||||
notify-success:
|
||||
needs: [integration-test-runner]
|
||||
runs-on: ubuntu-latest
|
||||
if: success()
|
||||
steps:
|
||||
- name: Slack notify
|
||||
id: slack
|
||||
uses: slackapi/slack-github-action@v1
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"workflow": "${{github.workflow}}",
|
||||
"message": "Plugin Integration tests have passed! :tada:"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
|
|
@ -0,0 +1,47 @@
|
|||
name: Update Manifest
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
update-manifest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: echo actor
|
||||
run: echo "actor is ${{ github.actor }}"
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
path: ./
|
||||
ssh-key: ${{ secrets.GH_SSH_KEY}}
|
||||
persist-credentials: true
|
||||
- name: update current plugin list
|
||||
run: |
|
||||
plugins=$(ls pkg/plugins | tr -s ' ' | sort)
|
||||
echo "got plugins: $plugins"
|
||||
echo '# 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.
|
||||
' > manifest
|
||||
echo "$plugins" >> manifest
|
||||
echo "manifest updated"
|
||||
echo "manifest contents:"
|
||||
cat manifest
|
||||
|
||||
- name: Commit changes
|
||||
run: |
|
||||
git config --local user.email "github-actions[bot]@noreply.example.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git add manifest || true
|
||||
git commit -m "update manifest" || true
|
||||
|
||||
- name: update manifest
|
||||
uses: ad-m/github-push-action@master
|
||||
with:
|
||||
ssh: true
|
||||
directory: .
|
||||
repository: opencost/opencost-plugins
|
||||
branch: main
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# OpenCost Plugins Committers and Maintainers
|
||||
|
||||
Official list of OpenCost Plugins Maintainers
|
||||
|
||||
## Maintainers
|
||||
|
||||
| Maintainer | GitHub ID | Affiliation | Email |
|
||||
| --------------- | --------- | ----------- | ----------- |
|
||||
| Nik Willwerth | @nik-kc | Kubecost | <nwillwerth@kubecost.com> |
|
||||
| Alex Meijer | @ameijer | Kubecost | <ameijer@kubecost.com> |
|
88
README.md
88
README.md
|
@ -1,2 +1,86 @@
|
|||
# opencost-plugins
|
||||
Plugins for extending OpenCost with new Custom Cost Sources
|
||||
|
||||
# OpenCost Plugins
|
||||
|
||||
OpenCost plugins make extending [OpenCost](https://github.com/opencost/opencost)’s coverage into new external cost sources (e.g. monitoring, data platforms, cloud services, and other SaaS solutions) available to the open source community. They allow for the ingestion of arbitrary cost data into OpenCost through conformance to the [FOCUS spec](https://focus.finops.org/).
|
||||
|
||||
# How plugins work
|
||||
|
||||
Any plugin released within this repository can be deployed alongside OpenCost. When deployed, OpenCost will query all installed plugins with a [request](https://github.com/opencost/opencost/blob/531641e608f404bbdc756c5dd291a44367053190/protos/customcost/messages.proto#L14-L22) for the following:
|
||||
- Start - timestamp
|
||||
- End - timestamp
|
||||
- Resolution - either hour or day
|
||||
|
||||
The plugins are expected to return a [response](https://github.com/opencost/opencost/blob/531641e608f404bbdc756c5dd291a44367053190/protos/customcost/messages.proto#L28-L54) that conforms to the aforementioned FOCUS spec for the given window and resolution. OpenCost will then store the response, and continue to request data for further time ranges.
|
||||
|
||||
The FOCUS spec is broken up into two parts:
|
||||
- [`CustomCost`](https://github.com/opencost/opencost/blob/531641e608f404bbdc756c5dd291a44367053190/protos/customcost/messages.proto#L56-L99)
|
||||
- [`CustomCostExtendedAttributes`](https://github.com/opencost/opencost/blob/531641e608f404bbdc756c5dd291a44367053190/protos/customcost/messages.proto#L101-L150)
|
||||
Plugin development only necessitates the implementation of the `CustomCost` response. Extended attributes are an optional response object. However, we high encourage the use of extended attributes, as doing so will assist us in developing the UX of said attributes within OpenCost.
|
||||
|
||||
See [this ExcaliDraw diagram](https://app.excalidraw.com/l/ABLQ24dkKai/CBEQtjH6Mr) for a more details overview of the plugin system
|
||||
|
||||
# Creating a new plugin
|
||||
|
||||
At the most basic level, all a plugin needs to do is gather cost data given a time window and resolution. The logistics of this are straightforward, but the complexity of the implementation will depend on the data source in question.
|
||||
|
||||
## Plugin setup
|
||||
- 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/pkg/plugins/datadog) for a reference to follow along with.
|
||||
- Create the plugin subdirectories:
|
||||
- `<repo>/pkg/plugins/<plugin>/cmd/main/`
|
||||
- This will contain the actual logic of the 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.
|
||||
- `<repo>/pkg/plugins/<plugin>/tests/`
|
||||
- Highly recommended, this will contain tests to validate the functionality of the plugin.
|
||||
- Initialize the subproject:
|
||||
- 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
|
||||
|
||||
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
|
||||
|
||||
Once the configuration is designed, it's time to write the plugin. Within `<repo>/<plugin>/cmd/main/>`, create `main.go`:
|
||||
- Create a `<plugin>Source` struct ([Datadog reference](https://github.com/opencost/opencost-plugins/blob/00809062196b79ce354a5cdafaba1d6ed3f132f9/datadog/cmd/main/main.go#L43-L47)).
|
||||
- Implement the [`CustomCostSource` interface](https://github.com/opencost/opencost/blob/531641e608f404bbdc756c5dd291a44367053190/core/pkg/plugin/plugin_interface.go#L12-L14) for your plugin source ([Datadog reference](https://github.com/opencost/opencost-plugins/blob/00809062196b79ce354a5cdafaba1d6ed3f132f9/datadog/cmd/main/main.go#L49-L88)). At the time of the writing of this guide, the only required function is `GetCustomCosts`, which takes in a [`CustomCostRequest`](https://github.com/opencost/opencost/blob/develop/protos/customcost/messages.proto#L14-L22) object and returns a list of [`CustomCostResponse`](https://github.com/opencost/opencost/blob/develop/protos/customcost/messages.proto#L28-L54) objects. Let's step through the Datadog reference implementation:
|
||||
- [First](https://github.com/opencost/opencost-plugins/blob/00809062196b79ce354a5cdafaba1d6ed3f132f9/datadog/cmd/main/main.go#L52), we split the requested window into sub-windows given the requested resolution. OpenCost has a convenience function to perform this split for us ([`GetWindows`](https://github.com/opencost/opencost/blob/b9f5e42f17ae5b1b05b722dd04502bd307a6a25c/core/pkg/opencost/window.go#L1084)).
|
||||
- [Next](https://github.com/opencost/opencost-plugins/blob/00809062196b79ce354a5cdafaba1d6ed3f132f9/datadog/cmd/main/main.go#L63), we grab some pricing data from the Datadog API to prepare for the next step.
|
||||
- [Penultimately](https://github.com/opencost/opencost-plugins/blob/00809062196b79ce354a5cdafaba1d6ed3f132f9/datadog/cmd/main/main.go#L75-L85), we iterate through each sub-window, grabbing the cost data from the Datadog API for each one.
|
||||
- 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.
|
||||
- Implement the `main` function:
|
||||
- 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/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)).
|
||||
- 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)
|
||||
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!
|
||||
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.
|
||||
|
||||
## Plugin system limitations
|
||||
- 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.
|
||||
- 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 -->
|
|
@ -1,428 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"os"
|
||||
|
||||
"github.com/DataDog/datadog-api-client-go/v2/api/datadog"
|
||||
"github.com/DataDog/datadog-api-client-go/v2/api/datadogV2"
|
||||
"golang.org/x/time/rate"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/hashicorp/go-plugin"
|
||||
datadogplugin "github.com/opencost/opencost-plugins/datadog/datadogplugin"
|
||||
"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"
|
||||
)
|
||||
|
||||
// URL of the Datadog pricing page
|
||||
const url = "https://aws.amazon.com/marketplace/pp/prodview-536p4hpqbajc2"
|
||||
|
||||
// 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: "datadog",
|
||||
}
|
||||
|
||||
// Implementation of CustomCostSource
|
||||
type DatadogCostSource struct {
|
||||
ddCtx context.Context
|
||||
usageApi *datadogV2.UsageMeteringApi
|
||||
rateLimiter *rate.Limiter
|
||||
}
|
||||
|
||||
func (d *DatadogCostSource) GetCustomCosts(req *pb.CustomCostRequest) []*pb.CustomCostResponse {
|
||||
results := []*pb.CustomCostResponse{}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
log.Debugf("fetching DD costs for window %v", target)
|
||||
result := d.getDDCostsForWindow(target, listPricing)
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
configFile, err := getConfigFilePath()
|
||||
if err != nil {
|
||||
log.Fatalf("error opening config file: %v", err)
|
||||
}
|
||||
|
||||
ddConfig, err := getDatadogConfig(configFile)
|
||||
if err != nil {
|
||||
log.Fatalf("error building DD config: %v", err)
|
||||
}
|
||||
|
||||
// datadog usage APIs allow 10 requests every 30 seconds
|
||||
rateLimiter := rate.NewLimiter(0.25, 5)
|
||||
ddCostSrc := DatadogCostSource{
|
||||
rateLimiter: rateLimiter,
|
||||
}
|
||||
ddCostSrc.ddCtx, ddCostSrc.usageApi = getDatadogClients(*ddConfig)
|
||||
|
||||
// pluginMap is the map of plugins we can dispense.
|
||||
var pluginMap = map[string]plugin.Plugin{
|
||||
"CustomCostSource": &ocplugin.CustomCostPlugin{Impl: &ddCostSrc},
|
||||
}
|
||||
|
||||
plugin.Serve(&plugin.ServeConfig{
|
||||
HandshakeConfig: handshakeConfig,
|
||||
Plugins: pluginMap,
|
||||
GRPCServer: plugin.DefaultGRPCServer,
|
||||
})
|
||||
}
|
||||
|
||||
func boilerplateDDCustomCost(win opencost.Window) pb.CustomCostResponse {
|
||||
return pb.CustomCostResponse{
|
||||
Metadata: map[string]string{"api_client_version": "v2"},
|
||||
CostSource: "observability",
|
||||
Domain: "datadog",
|
||||
Version: "v1",
|
||||
Currency: "USD",
|
||||
Start: timestamppb.New(*win.Start()),
|
||||
End: timestamppb.New(*win.End()),
|
||||
Errors: []string{},
|
||||
Costs: []*pb.CustomCost{},
|
||||
}
|
||||
}
|
||||
func (d *DatadogCostSource) getDDCostsForWindow(window opencost.Window, listPricing *datadogplugin.PricingInformation) *pb.CustomCostResponse {
|
||||
ccResp := boilerplateDDCustomCost(window)
|
||||
|
||||
nextPageId := "init"
|
||||
for morepages := true; morepages; morepages = (nextPageId != "") {
|
||||
params := datadogV2.NewGetHourlyUsageOptionalParameters()
|
||||
if nextPageId != "init" {
|
||||
params.PageNextRecordId = &nextPageId
|
||||
}
|
||||
if d.rateLimiter.Tokens() < 1.0 {
|
||||
log.Infof("datadog rate limit reached. holding request until rate capacity is back")
|
||||
}
|
||||
|
||||
err := d.rateLimiter.WaitN(context.TODO(), 2)
|
||||
if err != nil {
|
||||
log.Errorf("error waiting on rate limiter`: %v\n", err)
|
||||
ccResp.Errors = append(ccResp.Errors, err.Error())
|
||||
return &ccResp
|
||||
}
|
||||
|
||||
params.FilterTimestampEnd = window.End()
|
||||
resp, r, err := d.usageApi.GetHourlyUsage(d.ddCtx, *window.Start(), "all", *params)
|
||||
if err != nil {
|
||||
log.Errorf("Error when calling `UsageMeteringApi.GetHourlyUsage`: %v\n", err)
|
||||
log.Errorf("Full HTTP response: %v\n", r)
|
||||
ccResp.Errors = append(ccResp.Errors, err.Error())
|
||||
}
|
||||
|
||||
// many datadog usages are given in terms of a cumulative month to date usage
|
||||
// therefore, make a call for the hour before this hour to get a comparison
|
||||
// where needed
|
||||
params.FilterTimestampEnd = window.Start()
|
||||
toSub := window.End().Sub(*window.Start())
|
||||
respPriorWindow, r, err := d.usageApi.GetHourlyUsage(d.ddCtx, (*window.Start()).Add(-toSub), "all", *params)
|
||||
if err != nil {
|
||||
log.Errorf("Error when calling `UsageMeteringApi.GetHourlyUsage`: %v\n", err)
|
||||
log.Errorf("Full HTTP response: %v\n", r)
|
||||
ccResp.Errors = append(ccResp.Errors, err.Error())
|
||||
}
|
||||
|
||||
for index := range resp.Data {
|
||||
for indexMeas := range resp.Data[index].Attributes.Measurements {
|
||||
usageQty := float32(0.0)
|
||||
|
||||
if resp.Data[index].Attributes.Measurements[indexMeas].Value.IsSet() {
|
||||
var prior *datadogV2.HourlyUsageMeasurement
|
||||
if len(respPriorWindow.Data) > index {
|
||||
prior = &respPriorWindow.Data[index].Attributes.Measurements[indexMeas]
|
||||
} else {
|
||||
// then this is an out of bound access
|
||||
log.Warnf("could not get prior window data from timeframe %v, and measurement %v", window, resp.Data[index].Attributes.Measurements[indexMeas])
|
||||
log.Warnf("passing in nil prior window data")
|
||||
}
|
||||
usageQty = GetUsageQuantity(*resp.Data[index].Attributes.ProductFamily, &resp.Data[index].Attributes.Measurements[indexMeas], prior)
|
||||
}
|
||||
|
||||
if usageQty == 0.0 {
|
||||
log.Tracef("product %s/%s had 0 usage, not recording that cost", *resp.Data[index].Attributes.ProductFamily, *resp.Data[index].Attributes.Measurements[indexMeas].UsageType)
|
||||
continue
|
||||
}
|
||||
|
||||
desc, usageUnit, pricing, currency := getListingInfo(window, *resp.Data[index].Attributes.ProductFamily, *resp.Data[index].Attributes.Measurements[indexMeas].UsageType, listPricing)
|
||||
ccResp.Currency = currency
|
||||
cost := pb.CustomCost{
|
||||
Zone: *resp.Data[index].Attributes.Region,
|
||||
AccountName: *resp.Data[index].Attributes.OrgName,
|
||||
ChargeCategory: "usage",
|
||||
Description: desc,
|
||||
ResourceName: *resp.Data[index].Attributes.Measurements[indexMeas].UsageType,
|
||||
ResourceType: *resp.Data[index].Attributes.ProductFamily,
|
||||
Id: *resp.Data[index].Id,
|
||||
ProviderId: *resp.Data[index].Attributes.PublicId + "/" + *resp.Data[index].Attributes.Measurements[indexMeas].UsageType,
|
||||
Labels: map[string]string{},
|
||||
ListCost: usageQty * pricing,
|
||||
ListUnitPrice: pricing,
|
||||
UsageQuantity: usageQty,
|
||||
UsageUnit: usageUnit,
|
||||
ExtendedAttributes: nil,
|
||||
}
|
||||
ccResp.Costs = append(ccResp.Costs, &cost)
|
||||
}
|
||||
}
|
||||
if resp.Meta != nil && resp.Meta.Pagination != nil && resp.Meta.Pagination.NextRecordId.IsSet() {
|
||||
nextPageId = *resp.Meta.Pagination.NextRecordId.Get()
|
||||
} else {
|
||||
nextPageId = ""
|
||||
}
|
||||
}
|
||||
|
||||
return &ccResp
|
||||
}
|
||||
|
||||
// we have two basic types usages: cumulative and rate
|
||||
// rate usages are e.g., number of infra hosts, that have fixed costs per hour
|
||||
// cumulative usages are e.g., number of logs ingested, that have a fixed cost per unit
|
||||
// if a usage is cumulative, then suptract the usage in the hour prior to get the incremental usage
|
||||
// if a usage is a rate, then just return the usage
|
||||
func GetUsageQuantity(productFamily string, currentPeriodUsage, previousPeriodUsage *datadogV2.HourlyUsageMeasurement) float32 {
|
||||
curUsage := currentPeriodUsage.GetValue()
|
||||
if _, found := rateFamilies[productFamily]; found {
|
||||
// this family is a rate family, so just return the usage
|
||||
return float32(curUsage)
|
||||
}
|
||||
|
||||
prevUsage := int64(0)
|
||||
if previousPeriodUsage == nil {
|
||||
log.Warnf("previous period usage was nil, assuming 0 usage for that timeframe for family %s", productFamily)
|
||||
} else {
|
||||
prevUsage = previousPeriodUsage.GetValue()
|
||||
}
|
||||
|
||||
return float32(curUsage - prevUsage)
|
||||
}
|
||||
|
||||
// the public pricing used in the pricing list doesn't always match the usage reports
|
||||
// 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",
|
||||
|
||||
"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,
|
||||
}
|
||||
|
||||
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)
|
||||
pricing = pricingPerHour * float32(window.Duration().Hours())
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
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 = context.WithValue(
|
||||
ddctx,
|
||||
datadog.ContextServerVariables,
|
||||
map[string]string{"site": config.DDSite},
|
||||
)
|
||||
|
||||
keys := make(map[string]datadog.APIKey)
|
||||
|
||||
keys["apiKeyAuth"] = datadog.APIKey{Key: config.DDAPIKey}
|
||||
keys["appKeyAuth"] = datadog.APIKey{Key: config.DDAppKey}
|
||||
|
||||
ddctx = context.WithValue(
|
||||
ddctx,
|
||||
datadog.ContextAPIKeys,
|
||||
keys,
|
||||
)
|
||||
|
||||
configuration := datadog.NewConfiguration()
|
||||
apiClient := datadog.NewAPIClient(configuration)
|
||||
usageAPI := datadogV2.NewUsageMeteringApi(apiClient)
|
||||
return ddctx, usageAPI
|
||||
}
|
||||
|
||||
func getDatadogConfig(configFilePath string) (*datadogplugin.DatadogConfig, error) {
|
||||
var result datadogplugin.DatadogConfig
|
||||
bytes, err := os.ReadFile(configFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading config file for DD config @ %s: %v", configFilePath, err)
|
||||
}
|
||||
err = json.Unmarshal(bytes, &result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error marshaling json into DD config %v", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func getConfigFilePath() (string, error) {
|
||||
// plugins expect exactly 2 args: the executable itself,
|
||||
// and a path to the config file to use
|
||||
// all config for the plugin must come through the config file
|
||||
if len(os.Args) != 2 {
|
||||
return "", fmt.Errorf("plugins require 2 args: the plugin itself, and the full path to its config file. Got %d args", len(os.Args))
|
||||
}
|
||||
|
||||
_, err := os.Stat(os.Args[1])
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error reading config file at %s: %v", os.Args[1], err)
|
||||
}
|
||||
|
||||
return os.Args[1], nil
|
||||
}
|
||||
|
||||
func scrapeDatadogPrices(url string) (*datadogplugin.PricingInformation, error) {
|
||||
// Send a GET request to the URL
|
||||
response, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch the page: %v", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
// Check if the request was successful
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("failed to retrieve pricing page. Status code: %d", response.StatusCode)
|
||||
}
|
||||
b, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read pricing page body: %v", err)
|
||||
}
|
||||
res := datadogplugin.DatadogProJSON{}
|
||||
r := regexp.MustCompile(`var productDetailData = \s*(.*?)\s*;`)
|
||||
log.Debugf("got response: %s", string(b))
|
||||
matches := r.FindAllStringSubmatch(string(b), -1)
|
||||
if len(matches) != 1 {
|
||||
return nil, fmt.Errorf("requires exactly 1 product detail data, got %d", len(matches))
|
||||
}
|
||||
|
||||
log.Debugf("matches[0][1]:" + matches[0][1])
|
||||
err = json.Unmarshal([]byte(matches[0][1]), &res)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read pricing page body: %v", err)
|
||||
}
|
||||
|
||||
return &res.OfferData.PricingInformation, nil
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package datadog
|
||||
|
||||
type DatadogConfig struct {
|
||||
DDSite string `json:"datadog_site"`
|
||||
DDAPIKey string `json:"datadog_api_key"`
|
||||
DDAppKey string `json:"datadog_app_key"`
|
||||
}
|
14
justfile
14
justfile
|
@ -2,17 +2,25 @@ commonenv := "CGO_ENABLED=0"
|
|||
|
||||
version := `./tools/image-tag`
|
||||
commit := `git rev-parse --short HEAD`
|
||||
|
||||
pluginPaths := `find ./pkg/plugins -type f -iname "go.mod" -print0 | xargs -0 dirname | xargs -I{} basename {} | tr ' ' ',' | tr '\n' ','`
|
||||
default:
|
||||
just --list
|
||||
|
||||
# Run unit tests
|
||||
test-all-plugins:
|
||||
{{commonenv}} find . -type f -iname "go.mod" -print0 | xargs -0 -I{} ./tools/run-tests {}
|
||||
{{commonenv}} find ./pkg/plugins -type f -iname "go.mod" -print0 | xargs -0 -I{} ./tools/run-tests {}
|
||||
|
||||
build-all-plugins: clean test-all-plugins
|
||||
mkdir -p ./build
|
||||
find . -type f -iname "go.mod" -print0 | {{commonenv}} VERSION={{version}} COMMIT={{commit}} xargs -0 -I{} ./tools/build-plugins {}
|
||||
find ./pkg/plugins -type f -iname "go.mod" -print0 | {{commonenv}} VERSION={{version}} COMMIT={{commit}} xargs -0 -I{} ./tools/build-plugins {}
|
||||
|
||||
init-workspace:
|
||||
go work init
|
||||
find . -type f -iname "go.mod" -print0 | xargs -0 dirname | xargs -I{} go work use {}
|
||||
|
||||
integration-test-all-plugins:
|
||||
echo "pluginPaths: {{pluginPaths}}"
|
||||
{{commonenv}} go run pkg/test/pkg/executor/main/main.go --plugins={{pluginPaths}}
|
||||
|
||||
clean:
|
||||
rm -rf ./build
|
||||
|
|
|
@ -0,0 +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.
|
||||
|
||||
datadog
|
||||
mongodb-atlas
|
||||
openai
|
|
@ -0,0 +1,22 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func GetConfigFilePath() (string, error) {
|
||||
// plugins expect exactly 2 args: the executable itself,
|
||||
// and a path to the config file to use
|
||||
// all config for the plugin must come through the config file
|
||||
if len(os.Args) != 2 {
|
||||
return "", fmt.Errorf("plugins require 2 args: the plugin itself, and the full path to its config file. Got %d args", len(os.Args))
|
||||
}
|
||||
|
||||
_, err := os.Stat(os.Args[1])
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error reading config file at %s: %v", os.Args[1], err)
|
||||
}
|
||||
|
||||
return os.Args[1], nil
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module github.com/opencost/opencost-plugins/common
|
||||
|
||||
go 1.22.2
|
|
@ -0,0 +1,634 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
_nethttp "net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/DataDog/datadog-api-client-go/v2/api/datadog"
|
||||
"github.com/DataDog/datadog-api-client-go/v2/api/datadogV2"
|
||||
"github.com/agnivade/levenshtein"
|
||||
"golang.org/x/time/rate"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/DataDog/datadog-api-client-go/v2/api/datadogV1"
|
||||
"github.com/hashicorp/go-plugin"
|
||||
commonconfig "github.com/opencost/opencost-plugins/pkg/common/config"
|
||||
datadogplugin "github.com/opencost/opencost-plugins/pkg/plugins/datadog/datadogplugin"
|
||||
"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"
|
||||
)
|
||||
|
||||
// URL of the Datadog pricing page
|
||||
const url = "https://aws.amazon.com/marketplace/pp/prodview-536p4hpqbajc2"
|
||||
|
||||
// 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: "datadog",
|
||||
}
|
||||
|
||||
// Implementation of CustomCostSource
|
||||
type DatadogCostSource struct {
|
||||
ddCtx context.Context
|
||||
usageApi *datadogV2.UsageMeteringApi
|
||||
v1UsageApi *datadogV1.UsageMeteringApi
|
||||
rateLimiter *rate.Limiter
|
||||
}
|
||||
|
||||
func (d *DatadogCostSource) GetCustomCosts(req *pb.CustomCostRequest) []*pb.CustomCostResponse {
|
||||
results := []*pb.CustomCostResponse{}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
if target.Start().After(time.Now().UTC()) {
|
||||
log.Debugf("skipping future window %v", target)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debugf("fetching DD costs for window %v", target)
|
||||
result := d.getDDCostsForWindow(target, unitPricing)
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
configFile, err := commonconfig.GetConfigFilePath()
|
||||
if err != nil {
|
||||
log.Fatalf("error opening config file: %v", err)
|
||||
}
|
||||
|
||||
ddConfig, err := getDatadogConfig(configFile)
|
||||
if err != nil {
|
||||
log.Fatalf("error building DD config: %v", err)
|
||||
}
|
||||
log.SetLogLevel(ddConfig.DDLogLevel)
|
||||
// datadog usage APIs allow 10 requests every 30 seconds
|
||||
rateLimiter := rate.NewLimiter(0.1, 1)
|
||||
ddCostSrc := DatadogCostSource{
|
||||
rateLimiter: rateLimiter,
|
||||
}
|
||||
ddCostSrc.ddCtx, ddCostSrc.usageApi, ddCostSrc.v1UsageApi = getDatadogClients(*ddConfig)
|
||||
|
||||
// pluginMap is the map of plugins we can dispense.
|
||||
var pluginMap = map[string]plugin.Plugin{
|
||||
"CustomCostSource": &ocplugin.CustomCostPlugin{Impl: &ddCostSrc},
|
||||
}
|
||||
|
||||
plugin.Serve(&plugin.ServeConfig{
|
||||
HandshakeConfig: handshakeConfig,
|
||||
Plugins: pluginMap,
|
||||
GRPCServer: plugin.DefaultGRPCServer,
|
||||
})
|
||||
}
|
||||
|
||||
func boilerplateDDCustomCost(win opencost.Window) pb.CustomCostResponse {
|
||||
return pb.CustomCostResponse{
|
||||
Metadata: map[string]string{"api_client_version": "v2"},
|
||||
CostSource: "observability",
|
||||
Domain: "datadog",
|
||||
Version: "v1",
|
||||
Currency: "USD",
|
||||
Start: timestamppb.New(*win.Start()),
|
||||
End: timestamppb.New(*win.End()),
|
||||
Errors: []string{},
|
||||
Costs: []*pb.CustomCost{},
|
||||
}
|
||||
}
|
||||
func (d *DatadogCostSource) getDDCostsForWindow(window opencost.Window, listPricing map[string]billableCost) *pb.CustomCostResponse {
|
||||
ccResp := boilerplateDDCustomCost(window)
|
||||
costs := map[string]*pb.CustomCost{}
|
||||
nextPageId := "init"
|
||||
for morepages := true; morepages; morepages = (nextPageId != "") {
|
||||
params := datadogV2.NewGetHourlyUsageOptionalParameters()
|
||||
if nextPageId != "init" {
|
||||
params.PageNextRecordId = &nextPageId
|
||||
}
|
||||
|
||||
if d.rateLimiter.Tokens() < 1.0 {
|
||||
log.Infof("datadog rate limit reached. holding request until rate capacity is back")
|
||||
}
|
||||
|
||||
err := d.rateLimiter.WaitN(context.TODO(), 1)
|
||||
if err != nil {
|
||||
log.Errorf("error waiting on rate limiter`: %v\n", err)
|
||||
ccResp.Errors = append(ccResp.Errors, err.Error())
|
||||
return &ccResp
|
||||
}
|
||||
|
||||
maxTries := 5
|
||||
try := 1
|
||||
var resp datadogV2.HourlyUsageResponse
|
||||
for try <= maxTries {
|
||||
params.FilterTimestampEnd = window.End()
|
||||
var r *_nethttp.Response
|
||||
resp, r, err = d.usageApi.GetHourlyUsage(d.ddCtx, *window.Start(), "all", *params)
|
||||
if err != nil {
|
||||
log.Errorf("Error when calling `UsageMeteringApi.GetHourlyUsage`: %v\n", err)
|
||||
log.Errorf("Full HTTP response: %v\n", r)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
break
|
||||
} else {
|
||||
if strings.Contains(err.Error(), "429") {
|
||||
log.Errorf("rate limit reached, retrying...")
|
||||
}
|
||||
time.Sleep(30 * time.Second)
|
||||
try++
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("after calling `UsageMeteringApi.GetHourlyUsage` %d times, still getting error: %v\n", maxTries, err)
|
||||
ccResp.Errors = append(ccResp.Errors, err.Error())
|
||||
}
|
||||
|
||||
for index := range resp.Data {
|
||||
// each of these entries gives hourly data steps
|
||||
for indexMeas := range resp.Data[index].Attributes.Measurements {
|
||||
usageQty := float32(0.0)
|
||||
|
||||
if resp.Data[index].Attributes.Measurements[indexMeas].Value.IsSet() {
|
||||
usageQty = float32(resp.Data[index].Attributes.Measurements[indexMeas].GetValue())
|
||||
}
|
||||
|
||||
if usageQty == 0.0 {
|
||||
log.Tracef("product %s/%s had 0 usage, not recording that cost", *resp.Data[index].Attributes.ProductFamily, *resp.Data[index].Attributes.Measurements[indexMeas].UsageType)
|
||||
continue
|
||||
}
|
||||
|
||||
matched, pricing := matchUsageToPricing(*resp.Data[index].Attributes.Measurements[indexMeas].UsageType, listPricing)
|
||||
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
|
||||
if matched == "" {
|
||||
log.Infof("no pricing found for %s", *resp.Data[index].Attributes.Measurements[indexMeas].UsageType)
|
||||
continue
|
||||
}
|
||||
|
||||
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 {
|
||||
// we have not encountered this cost type for this window yet, so create a new cost entry
|
||||
cost := pb.CustomCost{
|
||||
Zone: *resp.Data[index].Attributes.Region,
|
||||
AccountName: *resp.Data[index].Attributes.OrgName,
|
||||
ChargeCategory: "usage",
|
||||
Description: "nil",
|
||||
ResourceName: *resp.Data[index].Attributes.Measurements[indexMeas].UsageType,
|
||||
ResourceType: *resp.Data[index].Attributes.ProductFamily,
|
||||
Id: *resp.Data[index].Id,
|
||||
ProviderId: provId,
|
||||
Labels: map[string]string{},
|
||||
ListCost: 0,
|
||||
ListUnitPrice: 0,
|
||||
BilledCost: billedCost,
|
||||
UsageQuantity: usageQty,
|
||||
UsageUnit: pricing.unit,
|
||||
ExtendedAttributes: nil,
|
||||
}
|
||||
|
||||
costs[provId] = &cost
|
||||
}
|
||||
}
|
||||
}
|
||||
if resp.Meta != nil && resp.Meta.Pagination != nil && resp.Meta.Pagination.NextRecordId.IsSet() {
|
||||
nextPageId = *resp.Meta.Pagination.NextRecordId.Get()
|
||||
} else {
|
||||
nextPageId = ""
|
||||
}
|
||||
}
|
||||
allCosts := []*pb.CustomCost{}
|
||||
for _, cost := range costs {
|
||||
allCosts = append(allCosts, cost)
|
||||
}
|
||||
ccResp.Costs = allCosts
|
||||
|
||||
// post processing
|
||||
// datadog's usage API sometimes provides usages that get counted multiple times
|
||||
// this post processing stage de-duplicates those usages and costs
|
||||
postProcess(&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) {
|
||||
if ccResp == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ccResp.Costs = processInfraHosts(ccResp.Costs)
|
||||
|
||||
ccResp.Costs = processLogUsage(ccResp.Costs)
|
||||
|
||||
// DBM queries have 200 * number of hosts included. We need to adjust the costs to reflect this
|
||||
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
|
||||
// the first 200 queries per host are free.
|
||||
// if that zeroes out the dbm queries, we remove the cost
|
||||
func adjustDBMQueries(costs []*pb.CustomCost) []*pb.CustomCost {
|
||||
totalFreeQueries := float32(0.0)
|
||||
for index := 0; index < len(costs); index++ {
|
||||
if costs[index].ResourceName == "dbm_host_count" {
|
||||
hostCount := costs[index].UsageQuantity
|
||||
totalFreeQueries += 200 * float32(hostCount)
|
||||
}
|
||||
}
|
||||
log.Debugf("total free queries: %f", totalFreeQueries)
|
||||
|
||||
for index := 0; index < len(costs); index++ {
|
||||
if costs[index].ResourceName == "dbm_queries_count" {
|
||||
costs[index].UsageQuantity -= totalFreeQueries
|
||||
log.Debugf("adjusted dbm queries: %v", costs[index])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for index := 0; index < len(costs); index++ {
|
||||
if costs[index].ResourceName == "dbm_queries_count" {
|
||||
if costs[index].UsageQuantity <= 0 {
|
||||
log.Debugf("removing cost %s because it has 0 usage", costs[index].ProviderId)
|
||||
costs = append(costs[:index], costs[index+1:]...)
|
||||
index = 0
|
||||
} else {
|
||||
// TODO else, multiply cost by the rate for extra queries
|
||||
costs[index].ListCost = 0.0
|
||||
costs[index].ListUnitPrice = 0.0
|
||||
costs[index].UsageUnit = "queries"
|
||||
}
|
||||
}
|
||||
}
|
||||
return costs
|
||||
}
|
||||
|
||||
// removes any items that have 0 usage or cost, either because of post processing or otherwise
|
||||
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++ {
|
||||
log.Tracef("POST - looking at cost %s with usage %f", costs[index].ResourceName, costs[index].UsageQuantity)
|
||||
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:]...)
|
||||
log.Tracef("POST - costs is now %d", len(costs))
|
||||
index = -1
|
||||
}
|
||||
}
|
||||
log.Tracef("POST -costs length after post processing: %d", len(costs))
|
||||
|
||||
return costs
|
||||
}
|
||||
|
||||
func processInfraHosts(costs []*pb.CustomCost) []*pb.CustomCost {
|
||||
// remove the container_count item
|
||||
for index := 0; index < len(costs); index++ {
|
||||
if costs[index].ResourceName == "container_count" {
|
||||
costs = append(costs[:index], costs[index+1:]...)
|
||||
index = 0
|
||||
}
|
||||
}
|
||||
|
||||
return costs
|
||||
}
|
||||
|
||||
func processLogUsage(costs []*pb.CustomCost) []*pb.CustomCost {
|
||||
|
||||
for index := range costs {
|
||||
if costs[index].ResourceName == "logs_live_indexed_events_15_day_count" {
|
||||
costs = append(costs[:index], costs[index+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// remove live indexed events count, that is covered by the other categories
|
||||
for index := range costs {
|
||||
if costs[index].ResourceName == "logs_live_indexed_count" {
|
||||
costs = append(costs[:index], costs[index+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var logsIndexed *pb.CustomCost
|
||||
for index := range costs {
|
||||
if costs[index].ResourceName == "indexed_events_count" {
|
||||
logsIndexed = costs[index]
|
||||
costs = append(costs[:index], costs[index+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if logsIndexed != nil {
|
||||
leftoverLogs := float32(0.0)
|
||||
for index := range costs {
|
||||
if costs[index].ResourceName == "logs_indexed_events_15_day_count" {
|
||||
leftoverLogs = float32(logsIndexed.UsageQuantity) - float32(costs[index].UsageQuantity)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
logsIndexed.Description = "other log events"
|
||||
logsIndexed.UsageQuantity = leftoverLogs
|
||||
logsIndexed.ResourceName = "other_log_events"
|
||||
logsIndexed.ListCost = leftoverLogs * logsIndexed.ListUnitPrice
|
||||
costs = append(costs, logsIndexed)
|
||||
}
|
||||
return costs
|
||||
}
|
||||
|
||||
func getDatadogClients(config datadogplugin.DatadogConfig) (context.Context, *datadogV2.UsageMeteringApi, *datadogV1.UsageMeteringApi) {
|
||||
ddctx := datadog.NewDefaultContext(context.Background())
|
||||
ddctx = context.WithValue(
|
||||
ddctx,
|
||||
datadog.ContextServerVariables,
|
||||
map[string]string{"site": config.DDSite},
|
||||
)
|
||||
|
||||
keys := make(map[string]datadog.APIKey)
|
||||
|
||||
keys["apiKeyAuth"] = datadog.APIKey{Key: config.DDAPIKey}
|
||||
keys["appKeyAuth"] = datadog.APIKey{Key: config.DDAppKey}
|
||||
|
||||
ddctx = context.WithValue(
|
||||
ddctx,
|
||||
datadog.ContextAPIKeys,
|
||||
keys,
|
||||
)
|
||||
|
||||
configuration := datadog.NewConfiguration()
|
||||
apiClient := datadog.NewAPIClient(configuration)
|
||||
usageAPI := datadogV2.NewUsageMeteringApi(apiClient)
|
||||
v1UsageAPI := datadogV1.NewUsageMeteringApi(apiClient)
|
||||
return ddctx, usageAPI, v1UsageAPI
|
||||
}
|
||||
|
||||
func getDatadogConfig(configFilePath string) (*datadogplugin.DatadogConfig, error) {
|
||||
var result datadogplugin.DatadogConfig
|
||||
bytes, err := os.ReadFile(configFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading config file for DD config @ %s: %v", configFilePath, err)
|
||||
}
|
||||
err = json.Unmarshal(bytes, &result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error marshaling json into DD config %v", err)
|
||||
}
|
||||
|
||||
if result.DDLogLevel == "" {
|
||||
result.DDLogLevel = "info"
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (d *DatadogCostSource) GetDDUnitPrices(windowStart time.Time) (map[string]billableCost, error) {
|
||||
|
||||
// DD estimated costs can be delayed 72 hours
|
||||
// so ensure we are going far enough back
|
||||
stableTimeframe := time.Now().UTC().Add(-3 * 24 * time.Hour)
|
||||
|
||||
targetMonth := time.Date(stableTimeframe.Year(), stableTimeframe.Month(), 1, 0, 0, 0, 0, time.UTC)
|
||||
targetMonthEnd := time.Date(stableTimeframe.Year(), stableTimeframe.Month()+1, 1, 0, 0, 0, 0, time.UTC)
|
||||
// first, get the billable usage for the month
|
||||
opts := datadogV1.GetUsageBillableSummaryOptionalParameters{
|
||||
Month: &targetMonth,
|
||||
}
|
||||
var respBillableUsage datadogV1.UsageBillableSummaryResponse
|
||||
var err error
|
||||
for try := 1; try <= 5; {
|
||||
respBillableUsage, _, err = d.v1UsageApi.GetUsageBillableSummary(d.ddCtx, opts)
|
||||
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("error getting usage billable usage summary: %v", err)
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
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, ""
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
datadogplugin "github.com/opencost/opencost-plugins/pkg/plugins/datadog/datadogplugin"
|
||||
"github.com/opencost/opencost/core/pkg/log"
|
||||
"github.com/opencost/opencost/core/pkg/model/pb"
|
||||
"github.com/opencost/opencost/core/pkg/util/timeutil"
|
||||
"golang.org/x/time/rate"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
func TestGetCustomCosts(t *testing.T) {
|
||||
// read necessary env vars. If any are missing, log warning and skip test
|
||||
ddSite := os.Getenv("DD_SITE")
|
||||
ddApiKey := os.Getenv("DD_API_KEY")
|
||||
ddAppKey := os.Getenv("DD_APPLICATION_KEY")
|
||||
|
||||
if ddSite == "" {
|
||||
log.Warnf("DD_SITE undefined, this needs to have the URL of your DD instance, skipping test")
|
||||
t.Skip()
|
||||
return
|
||||
}
|
||||
|
||||
if ddApiKey == "" {
|
||||
log.Warnf("DD_API_KEY undefined, skipping test")
|
||||
t.Skip()
|
||||
return
|
||||
}
|
||||
|
||||
if ddAppKey == "" {
|
||||
log.Warnf("DD_APPLICATION_KEY undefined, skipping test")
|
||||
t.Skip()
|
||||
return
|
||||
}
|
||||
|
||||
// write out config to temp file using contents of env vars
|
||||
config := datadogplugin.DatadogConfig{
|
||||
DDSite: ddSite,
|
||||
DDAPIKey: ddApiKey,
|
||||
DDAppKey: ddAppKey,
|
||||
}
|
||||
|
||||
rateLimiter := rate.NewLimiter(0.25, 5)
|
||||
ddCostSrc := DatadogCostSource{
|
||||
rateLimiter: rateLimiter,
|
||||
}
|
||||
ddCostSrc.ddCtx, ddCostSrc.usageApi, ddCostSrc.v1UsageApi = getDatadogClients(config)
|
||||
windowStart := time.Date(2024, 10, 16, 0, 0, 0, 0, time.UTC)
|
||||
// query for qty 2 of 1 hour windows
|
||||
windowEnd := time.Date(2024, 10, 17, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
req := &pb.CustomCostRequest{
|
||||
Start: timestamppb.New(windowStart),
|
||||
End: timestamppb.New(windowEnd),
|
||||
Resolution: durationpb.New(timeutil.Day),
|
||||
}
|
||||
|
||||
log.SetLogLevel("trace")
|
||||
resp := ddCostSrc.GetCustomCosts(req)
|
||||
|
||||
if len(resp) == 0 {
|
||||
t.Fatalf("empty response")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,221 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"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 datadog plugin")
|
||||
return false
|
||||
}
|
||||
|
||||
if len(respHourly) == 0 {
|
||||
log.Errorf("no hourly response received from datadog plugin")
|
||||
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 datadog: %v", multiErr)
|
||||
return false
|
||||
}
|
||||
|
||||
dbmCostsInRange := 0
|
||||
seenCosts := map[string]bool{}
|
||||
//verify that the returned costs are non zero
|
||||
for _, resp := range respDaily {
|
||||
if len(resp.Costs) == 0 && resp.Start.AsTime().After(time.Now().Truncate(24*time.Hour).Add(-1*time.Minute)) {
|
||||
log.Debugf("today's daily costs returned by plugin datadog are empty, skipping: %v", resp)
|
||||
continue
|
||||
}
|
||||
var costSum float32
|
||||
for _, cost := range resp.Costs {
|
||||
costSum += cost.GetBilledCost()
|
||||
seenCosts[cost.GetResourceName()] = true
|
||||
if cost.GetBilledCost() == 0 {
|
||||
log.Debugf("got zero cost for %v", cost)
|
||||
}
|
||||
if cost.GetBilledCost() > 100 {
|
||||
log.Errorf("daily cost returned by plugin datadog for %v is greater than 100", cost)
|
||||
return false
|
||||
}
|
||||
|
||||
//as of 10/2024, dbm hosts cost $84 a month or about $2.70. confirm that
|
||||
// range
|
||||
if cost.GetResourceName() == "dbm_host_count" {
|
||||
// filter out recent costs since those might not be full days worth
|
||||
if cost.GetBilledCost() > 2.5 && cost.GetBilledCost() < 3.0 {
|
||||
dbmCostsInRange++
|
||||
}
|
||||
}
|
||||
}
|
||||
if costSum == 0 {
|
||||
log.Errorf("daily costs returned by datadog plugin are zero")
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if dbmCostsInRange == 0 {
|
||||
log.Errorf("no dbm costs in expected range found in daily costs")
|
||||
return false
|
||||
}
|
||||
|
||||
expectedCosts := []string{
|
||||
"agent_host_count",
|
||||
"logs_indexed_events_15_day_count",
|
||||
"container_count_excl_agent",
|
||||
"dbm_host_count",
|
||||
}
|
||||
|
||||
for _, cost := range expectedCosts {
|
||||
if !seenCosts[cost] {
|
||||
log.Errorf("daily cost %s not found in plugin datadog response", cost)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if len(seenCosts) != len(expectedCosts) {
|
||||
log.Errorf("daily costs returned by plugin datadog do not equal expected costs")
|
||||
log.Errorf("seen costs: %v", seenCosts)
|
||||
log.Errorf("expected costs: %v", expectedCosts)
|
||||
|
||||
log.Errorf("response: %v", respDaily)
|
||||
return false
|
||||
}
|
||||
|
||||
// verify the domain matches the plugin name
|
||||
for _, resp := range respDaily {
|
||||
if resp.Domain != "datadog" {
|
||||
log.Errorf("daily domain returned by plugin datadog does not match plugin name")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
seenCosts = map[string]bool{}
|
||||
sumCosts := float32(0.0)
|
||||
for _, resp := range respHourly {
|
||||
|
||||
for _, cost := range resp.Costs {
|
||||
seenCosts[cost.GetResourceName()] = true
|
||||
sumCosts += cost.GetBilledCost()
|
||||
if cost.GetBilledCost() > 100 {
|
||||
log.Errorf("hourly cost returned by plugin datadog for %v is greater than 100", cost)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if sumCosts == 0 {
|
||||
log.Errorf("hourly costs returned by datadog plugin are zero")
|
||||
return false
|
||||
}
|
||||
|
||||
for _, cost := range expectedCosts {
|
||||
if !seenCosts[cost] {
|
||||
log.Errorf("daily cost %s not found in plugin datadog response", cost)
|
||||
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
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package datadog
|
||||
|
||||
type DatadogConfig struct {
|
||||
DDSite string `json:"datadog_site"`
|
||||
DDAPIKey string `json:"datadog_api_key"`
|
||||
DDAppKey string `json:"datadog_app_key"`
|
||||
DDLogLevel string `json:"log_level"`
|
||||
}
|
|
@ -1,11 +1,15 @@
|
|||
module github.com/opencost/opencost-plugins/datadog
|
||||
module github.com/opencost/opencost-plugins/pkg/plugins/datadog
|
||||
|
||||
go 1.21.6
|
||||
go 1.22.2
|
||||
|
||||
replace github.com/opencost/opencost-plugins/pkg/common => ../../common
|
||||
|
||||
require (
|
||||
github.com/DataDog/datadog-api-client-go/v2 v2.23.0
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
|
||||
github.com/hashicorp/go-plugin v1.6.0
|
||||
github.com/opencost/opencost-plugins/datadog v0.0.0-20240429172518-a50cd1290864
|
||||
github.com/opencost/opencost-plugins/pkg/common v0.0.0-00010101000000-000000000000
|
||||
github.com/opencost/opencost-plugins/test v0.0.0-20240307142929-df4df8ee69fa
|
||||
github.com/opencost/opencost/core v0.0.0-20240307141548-816f98c9051a
|
||||
golang.org/x/time v0.5.0
|
||||
|
@ -14,6 +18,7 @@ require (
|
|||
|
||||
require (
|
||||
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/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
|
@ -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/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
|
||||
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/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
|
@ -82,6 +84,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
|||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
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-plugins/datadog v0.0.0-20240429172518-a50cd1290864 h1:RfjDGBV2GfsB2jgJ1+0N+TeSysg080tGxAyd9pz/QIc=
|
||||
github.com/opencost/opencost-plugins/datadog v0.0.0-20240429172518-a50cd1290864/go.mod h1:8zKr+wkfcFJeS955K4wyQ8FiSdNQHSl+ZZajs5tUkyk=
|
||||
github.com/opencost/opencost-plugins/test v0.0.0-20240307142929-df4df8ee69fa h1:hzsLLTOech6pPdWMtQT/D9pa8eZu52VWKqnPdH4mMZQ=
|
||||
github.com/opencost/opencost-plugins/test v0.0.0-20240307142929-df4df8ee69fa/go.mod h1:6B39vhLt/C97HxMoTZehNF9cf9z7qRd9EzKUYKgvPg4=
|
||||
github.com/opencost/opencost/core v0.0.0-20240307141548-816f98c9051a h1:m6sesjHd7phuhoWhrCXrzLKHJbAdlH0Q07Uvpbgl4G0=
|
|
@ -3,6 +3,7 @@ package tests
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/opencost/opencost/core/pkg/util/timeutil"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
@ -19,10 +20,125 @@ import (
|
|||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// this test gets DD keys from env vars, writes a config file
|
||||
// then it invokes the plugin and validates some basics
|
||||
// in the response
|
||||
func TestDDCostRetrieval(t *testing.T) {
|
||||
func TestDDCostRetrievalListCost(t *testing.T) {
|
||||
// query for qty 2 of 1 hour windows
|
||||
windowStart := time.Date(2024, 3, 8, 0, 0, 0, 0, time.UTC)
|
||||
windowEnd := time.Date(2024, 3, 8, 2, 0, 0, 0, time.UTC)
|
||||
|
||||
response := getResponse(t, windowStart, windowEnd, time.Hour)
|
||||
|
||||
// confirm no errors in result
|
||||
if len(response) == 0 {
|
||||
t.Fatalf("empty response")
|
||||
}
|
||||
for _, resp := range response {
|
||||
if len(resp.Errors) > 0 {
|
||||
t.Fatalf("got errors in response: %v", resp.Errors)
|
||||
}
|
||||
}
|
||||
|
||||
result, err := json.MarshalIndent(response, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("error json-ing response: %v", err)
|
||||
}
|
||||
|
||||
t.Log(string(result))
|
||||
// confirm results have correct provider
|
||||
for _, resp := range response {
|
||||
if resp.Domain != "datadog" {
|
||||
t.Fatalf("unexpected domain. expected datadog, got %s", resp.Domain)
|
||||
}
|
||||
}
|
||||
|
||||
// check some attributes of the cost response
|
||||
for _, resp := range response {
|
||||
// confirm there are > 0 custom costs
|
||||
if len(resp.Costs) < 1 {
|
||||
t.Fatalf("expect non-zero costs in response.")
|
||||
}
|
||||
|
||||
for _, cost := range resp.Costs {
|
||||
if cost.ResourceType == "indexed_logs" {
|
||||
// check for sane values fo a rate-priced resource
|
||||
if cost.ListCost > 1000 {
|
||||
costDump := spew.Sdump(cost)
|
||||
t.Log(costDump)
|
||||
t.Fatalf("unexpectedly high cost for indexed logs: %f", cost.ListCost)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFuturism(t *testing.T) {
|
||||
// query for the future
|
||||
windowStart := time.Now().UTC().Truncate(time.Hour).Add(time.Hour)
|
||||
windowEnd := windowStart.Add(time.Hour)
|
||||
|
||||
response := getResponse(t, windowStart, windowEnd, time.Hour)
|
||||
|
||||
// when we query for data in the future, we expect to get back no data AND no errors
|
||||
if len(response) > 0 {
|
||||
t.Fatalf("got non-empty response")
|
||||
}
|
||||
for _, resp := range response {
|
||||
if len(resp.Errors) > 0 {
|
||||
t.Fatalf("got errors in response: %v", resp.Errors)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDDCostRetrievalBilledCost(t *testing.T) {
|
||||
// query for qty 2 of 1 hour windows
|
||||
windowStart := time.Date(2024, 3, 16, 0, 0, 0, 0, time.UTC)
|
||||
windowEnd := time.Date(2024, 3, 17, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
response := getResponse(t, windowStart, windowEnd, timeutil.Day)
|
||||
|
||||
// confirm no errors in result
|
||||
if len(response) == 0 {
|
||||
t.Fatalf("empty response")
|
||||
}
|
||||
for _, resp := range response {
|
||||
if len(resp.Errors) > 0 {
|
||||
t.Fatalf("got errors in response: %v", resp.Errors)
|
||||
}
|
||||
}
|
||||
|
||||
result, err := json.MarshalIndent(response, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("error json-ing response: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println(string(result))
|
||||
// confirm results have correct provider
|
||||
for _, resp := range response {
|
||||
if resp.Domain != "datadog" {
|
||||
t.Fatalf("unexpected domain. expected datadog, got %s", resp.Domain)
|
||||
}
|
||||
}
|
||||
|
||||
// check some attributes of the cost response
|
||||
var totalBilledCost float32 = 0.0
|
||||
for _, resp := range response {
|
||||
// confirm there are > 0 custom costs
|
||||
if len(resp.Costs) < 1 {
|
||||
t.Fatalf("expect non-zero costs in response.")
|
||||
}
|
||||
|
||||
for _, cost := range resp.Costs {
|
||||
totalBilledCost += cost.BilledCost
|
||||
}
|
||||
}
|
||||
|
||||
if totalBilledCost == 0 {
|
||||
responseDump := spew.Sdump(response)
|
||||
t.Log(responseDump)
|
||||
t.Fatalf("unexpectedly low total billed cost: %f", totalBilledCost)
|
||||
}
|
||||
}
|
||||
|
||||
func getResponse(t *testing.T, windowStart, windowEnd time.Time, step time.Duration) []*pb.CustomCostResponse {
|
||||
// read necessary env vars. If any are missing, log warning and skip test
|
||||
ddSite := os.Getenv("DD_SITE")
|
||||
ddApiKey := os.Getenv("DD_API_KEY")
|
||||
|
@ -31,19 +147,19 @@ func TestDDCostRetrieval(t *testing.T) {
|
|||
if ddSite == "" {
|
||||
log.Warnf("DD_SITE undefined, this needs to have the URL of your DD instance, skipping test")
|
||||
t.Skip()
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
if ddApiKey == "" {
|
||||
log.Warnf("DD_API_KEY undefined, skipping test")
|
||||
t.Skip()
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
if ddAppKey == "" {
|
||||
log.Warnf("DD_APPLICATION_KEY undefined, skipping test")
|
||||
t.Skip()
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// write out config to temp file using contents of env vars
|
||||
|
@ -74,59 +190,11 @@ func TestDDCostRetrieval(t *testing.T) {
|
|||
parent := filepath.Dir(filename)
|
||||
pluginRoot := filepath.Dir(parent)
|
||||
pluginFile := pluginRoot + "/cmd/main/main.go"
|
||||
windowStart := time.Date(2024, 3, 8, 0, 0, 0, 0, time.UTC)
|
||||
// query for qty 2 of 1 hour windows
|
||||
windowEnd := time.Date(2024, 3, 8, 2, 0, 0, 0, time.UTC)
|
||||
|
||||
req := pb.CustomCostRequest{
|
||||
Start: timestamppb.New(windowStart),
|
||||
End: timestamppb.New(windowEnd),
|
||||
Resolution: durationpb.New(time.Hour),
|
||||
Resolution: durationpb.New(step),
|
||||
}
|
||||
response := harness.InvokePlugin(file.Name(), pluginFile, &req)
|
||||
|
||||
// confirm no errors in result
|
||||
if len(response) == 0 {
|
||||
t.Fatalf("empty response")
|
||||
}
|
||||
for _, resp := range response {
|
||||
if len(resp.Errors) > 0 {
|
||||
t.Fatalf("got errors in response: %v", resp.Errors)
|
||||
}
|
||||
}
|
||||
|
||||
result, err := json.MarshalIndent(response, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("error json-ing response: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println(string(result))
|
||||
// confirm results have correct provider
|
||||
for _, resp := range response {
|
||||
if resp.Domain != "datadog" {
|
||||
t.Fatalf("unexpected domain. expected datadog, got %s", resp.Domain)
|
||||
}
|
||||
}
|
||||
|
||||
// check some attributes of the cost response
|
||||
for _, resp := range response {
|
||||
// confirm there are > 0 custom costs
|
||||
if len(resp.Costs) < 1 {
|
||||
t.Fatalf("expect non-zero costs in response.")
|
||||
}
|
||||
|
||||
for _, cost := range resp.Costs {
|
||||
if cost.ResourceType == "indexed_logs" {
|
||||
// check for sane values fo a rate-priced resource
|
||||
if cost.ListCost > 1000 {
|
||||
spew.Dump(
|
||||
cost,
|
||||
)
|
||||
t.Fatalf("unexpectedly high cost for indexed logs: %f", cost.ListCost)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return harness.InvokePlugin(file.Name(), pluginFile, &req)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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"`
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
openaiplugin "github.com/opencost/opencost-plugins/pkg/plugins/openai/openaiplugin"
|
||||
"github.com/opencost/opencost/core/pkg/log"
|
||||
"github.com/opencost/opencost/core/pkg/model/pb"
|
||||
"github.com/opencost/opencost/core/pkg/util/timeutil"
|
||||
"golang.org/x/time/rate"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
func TestGetCustomCosts(t *testing.T) {
|
||||
// read necessary env vars. If any are missing, log warning and skip test
|
||||
oaiApiKey := os.Getenv("OAI_API_KEY")
|
||||
if oaiApiKey == "" {
|
||||
log.Warnf("OAI_API_KEY undefined, skipping test")
|
||||
t.Skip()
|
||||
return
|
||||
}
|
||||
|
||||
//set up config
|
||||
config := openaiplugin.OpenAIConfig{
|
||||
APIKey: oaiApiKey,
|
||||
}
|
||||
|
||||
rateLimiter := rate.NewLimiter(1, 5)
|
||||
oaiCostSrc := OpenAICostSource{
|
||||
rateLimiter: rateLimiter,
|
||||
config: &config,
|
||||
}
|
||||
|
||||
windowStart := time.Date(2024, 10, 9, 0, 0, 0, 0, time.UTC)
|
||||
// query for qty 2 of 1 hour windows
|
||||
windowEnd := time.Date(2024, 10, 10, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
req := &pb.CustomCostRequest{
|
||||
Start: timestamppb.New(windowStart),
|
||||
End: timestamppb.New(windowEnd),
|
||||
Resolution: durationpb.New(timeutil.Day),
|
||||
}
|
||||
|
||||
log.SetLogLevel("debug")
|
||||
resp := oaiCostSrc.GetCustomCosts(req)
|
||||
|
||||
if len(resp) == 0 {
|
||||
t.Fatalf("empty response")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,361 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hashicorp/go-plugin"
|
||||
commonconfig "github.com/opencost/opencost-plugins/pkg/common/config"
|
||||
openaiplugin "github.com/opencost/opencost-plugins/pkg/plugins/openai/openaiplugin"
|
||||
"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"
|
||||
"github.com/opencost/opencost/core/pkg/util/timeutil"
|
||||
)
|
||||
|
||||
// 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: "openai",
|
||||
}
|
||||
|
||||
const openAIUsageURLFmt = "https://api.openai.com/v1/usage?date=%s"
|
||||
const openAIBillingURLFmt = "https://api.openai.com/v1/dashboard/billing/usage/export?exclude_project_costs=false&file_format=json&new_endpoint=true&project_id&start_date=%s&end_date=%s"
|
||||
const openAIAPIDateFormat = "2006-01-02"
|
||||
|
||||
// Implementation of CustomCostSource
|
||||
type OpenAICostSource struct {
|
||||
rateLimiter *rate.Limiter
|
||||
config *openaiplugin.OpenAIConfig
|
||||
}
|
||||
|
||||
func (d *OpenAICostSource) GetCustomCosts(req *pb.CustomCostRequest) []*pb.CustomCostResponse {
|
||||
results := []*pb.CustomCostResponse{}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if req.Resolution.AsDuration() != timeutil.Day {
|
||||
log.Infof("openai plugin only supports daily resolution")
|
||||
return results
|
||||
}
|
||||
|
||||
for _, target := range targets {
|
||||
// don't allow future request
|
||||
if target.Start().After(time.Now().UTC()) {
|
||||
log.Debugf("skipping future window %v", target)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debugf("fetching Open AI costs for window %v", target)
|
||||
result := d.getOpenAICostsForWindow(target)
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
configFile, err := commonconfig.GetConfigFilePath()
|
||||
if err != nil {
|
||||
log.Fatalf("error opening config file: %v", err)
|
||||
}
|
||||
|
||||
oaiConfig, err := getOpenAIConfig(configFile)
|
||||
if err != nil {
|
||||
log.Fatalf("error building OpenAI config: %v", err)
|
||||
}
|
||||
log.SetLogLevel(oaiConfig.LogLevel)
|
||||
// rate limit to 1 request per second
|
||||
rateLimiter := rate.NewLimiter(0.5, 1)
|
||||
oaiCostSrc := OpenAICostSource{
|
||||
rateLimiter: rateLimiter,
|
||||
config: oaiConfig,
|
||||
}
|
||||
|
||||
// pluginMap is the map of plugins we can dispense.
|
||||
var pluginMap = map[string]plugin.Plugin{
|
||||
"CustomCostSource": &ocplugin.CustomCostPlugin{Impl: &oaiCostSrc},
|
||||
}
|
||||
|
||||
plugin.Serve(&plugin.ServeConfig{
|
||||
HandshakeConfig: handshakeConfig,
|
||||
Plugins: pluginMap,
|
||||
GRPCServer: plugin.DefaultGRPCServer,
|
||||
})
|
||||
}
|
||||
|
||||
func boilerplateOpenAICustomCost(win opencost.Window) pb.CustomCostResponse {
|
||||
return pb.CustomCostResponse{
|
||||
Metadata: map[string]string{"api_client_version": "v1"},
|
||||
CostSource: "AI",
|
||||
Domain: "openai",
|
||||
Version: "v1",
|
||||
Currency: "USD",
|
||||
Start: timestamppb.New(*win.Start()),
|
||||
End: timestamppb.New(*win.End()),
|
||||
Errors: []string{},
|
||||
Costs: []*pb.CustomCost{},
|
||||
}
|
||||
}
|
||||
func (d *OpenAICostSource) getOpenAICostsForWindow(window opencost.Window) *pb.CustomCostResponse {
|
||||
ccResp := boilerplateOpenAICustomCost(window)
|
||||
|
||||
oaiTokenUsages, err := d.getOpenAITokenUsages(*window.Start())
|
||||
if err != nil {
|
||||
ccResp.Errors = append(ccResp.Errors, fmt.Sprintf("error getting OpenAI token usages: %v", err))
|
||||
}
|
||||
|
||||
oaiBilling, err := d.getOpenAIBilling(*window.Start(), *window.End())
|
||||
if err != nil {
|
||||
ccResp.Errors = append(ccResp.Errors, fmt.Sprintf("error getting OpenAI billing data: %v", err))
|
||||
}
|
||||
|
||||
customCosts, err := getCustomCostsFromUsageAndBilling(oaiTokenUsages, oaiBilling)
|
||||
if err != nil {
|
||||
ccResp.Errors = append(ccResp.Errors, fmt.Sprintf("error converting API responses into custom costs: %v", err))
|
||||
}
|
||||
ccResp.Costs = customCosts
|
||||
|
||||
return &ccResp
|
||||
}
|
||||
|
||||
func getCustomCostsFromUsageAndBilling(usage *openaiplugin.OpenAIUsage, billing *openaiplugin.OpenAIBilling) ([]*pb.CustomCost, error) {
|
||||
customCosts := []*pb.CustomCost{}
|
||||
|
||||
tokenMap := buildTokenMap(usage)
|
||||
for _, billingEntry := range billing.Data {
|
||||
tokenMapKey := strings.ReplaceAll(strings.ToLower(billingEntry.Name), "-", "")
|
||||
tokenMapKey = strings.ReplaceAll(tokenMapKey, " ", "")
|
||||
tokenMapKey = strings.ReplaceAll(tokenMapKey, "_", "")
|
||||
|
||||
tokenCount, ok := tokenMap[tokenMapKey]
|
||||
if !ok {
|
||||
log.Debugf("no token usage found for %s", billingEntry.Name)
|
||||
tokenCount = -1
|
||||
}
|
||||
|
||||
extendedAttrs := pb.CustomCostExtendedAttributes{
|
||||
AccountId: &billingEntry.OrganizationID,
|
||||
SubAccountId: &billingEntry.ProjectID,
|
||||
}
|
||||
customCost := pb.CustomCost{
|
||||
BilledCost: float32(billingEntry.CostInMajor),
|
||||
AccountName: billingEntry.OrganizationName,
|
||||
ChargeCategory: "Usage",
|
||||
Description: fmt.Sprintf("OpenAI usage for model %s", billingEntry.Name),
|
||||
ResourceName: billingEntry.Name,
|
||||
ResourceType: "AI Model",
|
||||
Id: uuid.New().String(),
|
||||
ProviderId: fmt.Sprintf("%s/%s/%s", billingEntry.OrganizationID, billingEntry.ProjectID, billingEntry.Name),
|
||||
UsageQuantity: float32(tokenCount),
|
||||
UsageUnit: "tokens - All snapshots, all projects",
|
||||
ExtendedAttributes: &extendedAttrs,
|
||||
}
|
||||
|
||||
customCosts = append(customCosts, &customCost)
|
||||
}
|
||||
|
||||
return customCosts, nil
|
||||
}
|
||||
|
||||
var snapshotRe = regexp.MustCompile(`-\d{4}-\d{2}-\d{2}|-`)
|
||||
|
||||
func buildTokenMap(usage *openaiplugin.OpenAIUsage) map[string]int {
|
||||
tokenMap := make(map[string]int)
|
||||
if usage == nil {
|
||||
return tokenMap
|
||||
}
|
||||
for _, usageData := range usage.Data {
|
||||
key := snapshotRe.ReplaceAllString(usageData.SnapshotID, "")
|
||||
key = strings.ToLower(key)
|
||||
if _, ok := tokenMap[key]; !ok {
|
||||
tokenMap[key] = 0
|
||||
}
|
||||
|
||||
tokenMap[key] += (usageData.NGeneratedTokensTotal + usageData.NContextTokensTotal)
|
||||
}
|
||||
return tokenMap
|
||||
}
|
||||
|
||||
func (d *OpenAICostSource) getOpenAIBilling(start time.Time, end time.Time) (*openaiplugin.OpenAIBilling, error) {
|
||||
client := &http.Client{}
|
||||
openAIBillingURL := fmt.Sprintf(openAIBillingURLFmt, start.Format(openAIAPIDateFormat), end.Format(openAIAPIDateFormat))
|
||||
log.Debugf("fetching OpenAI billing data from %s", openAIBillingURL)
|
||||
var errReq error
|
||||
var resp *http.Response
|
||||
for i := 0; i < 3; i++ {
|
||||
err := d.rateLimiter.Wait(context.Background())
|
||||
if err != nil {
|
||||
log.Warnf("error waiting for rate limiter: %v", err)
|
||||
return nil, fmt.Errorf("error waiting for rate limiter: %v", err)
|
||||
}
|
||||
var req *http.Request
|
||||
req, errReq = http.NewRequest("GET", openAIBillingURL, nil)
|
||||
if errReq != nil {
|
||||
log.Warnf("error creating billing export request: %v", errReq)
|
||||
log.Infof("retrying request after 30s")
|
||||
time.Sleep(30 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.config.APIKey))
|
||||
|
||||
resp, errReq = client.Do(req)
|
||||
if errReq != nil {
|
||||
log.Warnf("error doing billing export request: %v", errReq)
|
||||
log.Warnf("retrying requestafter 30s")
|
||||
time.Sleep(30 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
bodyString := "<empty>"
|
||||
if err != nil {
|
||||
log.Warnf("error reading body of non-200 response: %v", err)
|
||||
} else {
|
||||
bodyString = string(bodyBytes)
|
||||
}
|
||||
|
||||
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.Infof("retrying request after 30s")
|
||||
time.Sleep(30 * time.Second)
|
||||
continue
|
||||
} else {
|
||||
errReq = nil
|
||||
}
|
||||
// request was successful, break out of loop
|
||||
break
|
||||
}
|
||||
|
||||
if errReq != nil {
|
||||
return nil, fmt.Errorf("error making request after retries: %v", errReq)
|
||||
}
|
||||
var billingData openaiplugin.OpenAIBilling
|
||||
if err := json.NewDecoder(resp.Body).Decode(&billingData); err != nil {
|
||||
return nil, fmt.Errorf("error decoding billing export response: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
for i := range billingData.Data {
|
||||
asFloat, err := strconv.ParseFloat(billingData.Data[i].CostInMajorStr, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing cost: %v", err)
|
||||
}
|
||||
billingData.Data[i].CostInMajor = asFloat
|
||||
}
|
||||
|
||||
return &billingData, nil
|
||||
}
|
||||
|
||||
func (d *OpenAICostSource) getOpenAITokenUsages(targetTime time.Time) (*openaiplugin.OpenAIUsage, error) {
|
||||
client := &http.Client{}
|
||||
|
||||
openAIUsageURL := fmt.Sprintf(openAIUsageURLFmt, targetTime.Format(openAIAPIDateFormat))
|
||||
log.Debugf("fetching OpenAI usage data from %s", openAIUsageURL)
|
||||
var errReq error
|
||||
var resp *http.Response
|
||||
for i := 0; i < 3; i++ {
|
||||
errReq = nil
|
||||
err := d.rateLimiter.Wait(context.Background())
|
||||
if err != nil {
|
||||
log.Warnf("error waiting for rate limiter: %v", err)
|
||||
return nil, fmt.Errorf("error waiting for rate limiter: %v", err)
|
||||
}
|
||||
var req *http.Request
|
||||
req, errReq = http.NewRequest("GET", openAIUsageURL, nil)
|
||||
if errReq != nil {
|
||||
log.Warnf("error creating usage request: %v", errReq)
|
||||
log.Warnf("retrying request after 30s")
|
||||
time.Sleep(30 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.config.APIKey))
|
||||
|
||||
resp, errReq = client.Do(req)
|
||||
if errReq != nil {
|
||||
log.Warnf("error doing token request: %v", errReq)
|
||||
log.Infof("retrying request after 30s")
|
||||
time.Sleep(30 * time.Second)
|
||||
continue
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
errReq = fmt.Errorf("received non-200 response for token usage request: %d", resp.StatusCode)
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
bodyString := "<empty>"
|
||||
if err != nil {
|
||||
log.Warnf("error reading body of non-200 response: %v", err)
|
||||
} else {
|
||||
bodyString = string(bodyBytes)
|
||||
}
|
||||
log.Warnf("got non-200 response for token usage request: %d, body is: %s", resp.StatusCode, bodyString)
|
||||
log.Infof("retrying request after 30s")
|
||||
time.Sleep(30 * time.Second)
|
||||
continue
|
||||
} else {
|
||||
errReq = nil
|
||||
}
|
||||
// request was successful, break out of loop
|
||||
break
|
||||
}
|
||||
|
||||
if errReq != nil {
|
||||
return nil, fmt.Errorf("error making request after retries: %v", errReq)
|
||||
}
|
||||
|
||||
var usageData openaiplugin.OpenAIUsage
|
||||
if err := json.NewDecoder(resp.Body).Decode(&usageData); err != nil {
|
||||
return nil, fmt.Errorf("error decoding response: %v", err)
|
||||
}
|
||||
|
||||
return &usageData, nil
|
||||
}
|
||||
|
||||
func getOpenAIConfig(configFilePath string) (*openaiplugin.OpenAIConfig, error) {
|
||||
var result openaiplugin.OpenAIConfig
|
||||
bytes, err := os.ReadFile(configFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading config file for openai config @ %s: %v", configFilePath, err)
|
||||
}
|
||||
err = json.Unmarshal(bytes, &result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error marshaling json into openai config %v", err)
|
||||
}
|
||||
|
||||
if result.LogLevel == "" {
|
||||
result.LogLevel = "info"
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"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 openai plugin")
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
// check if any errors occurred
|
||||
if multiErr != nil {
|
||||
log.Errorf("Errors occurred during plugin testing for open ai: %v", multiErr)
|
||||
return false
|
||||
}
|
||||
seenCosts := map[string]bool{}
|
||||
var costSum float32
|
||||
//verify that the returned costs are non zero
|
||||
for _, resp := range respDaily {
|
||||
if len(resp.Costs) == 0 && resp.Start.AsTime().After(time.Now().Truncate(24*time.Hour).Add(-1*time.Minute)) {
|
||||
log.Debugf("today's daily costs returned by plugin openai are empty, skipping: %v", resp)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, cost := range resp.Costs {
|
||||
costSum += cost.GetBilledCost()
|
||||
seenCosts[cost.GetResourceName()] = true
|
||||
if cost.GetBilledCost() == 0 {
|
||||
log.Debugf("got zero cost for %v", cost)
|
||||
}
|
||||
if cost.GetBilledCost() > 2 {
|
||||
log.Errorf("daily cost returned by plugin openai for %v is greater than 1", cost)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if costSum == 0 {
|
||||
log.Errorf("daily costs returned by openai plugin are zero")
|
||||
return false
|
||||
}
|
||||
expectedCosts := []string{
|
||||
"GPT-4o mini",
|
||||
"GPT-4o",
|
||||
}
|
||||
|
||||
for _, cost := range expectedCosts {
|
||||
if !seenCosts[cost] {
|
||||
log.Errorf("daily cost %s not found in plugin openai response", cost)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// verify the domain matches the plugin name
|
||||
for _, resp := range respDaily {
|
||||
if resp.Domain != "openai" {
|
||||
log.Errorf("daily domain returned by plugin openai does not match plugin name")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if len(seenCosts) < len(expectedCosts)-1 || len(seenCosts) > len(expectedCosts)+1 {
|
||||
log.Errorf("daily costs returned by openai plugin are very different than expected")
|
||||
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
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
module github.com/opencost/opencost-plugins/pkg/plugins/openai
|
||||
|
||||
go 1.23
|
||||
|
||||
toolchain go1.23.1
|
||||
|
||||
replace github.com/opencost/opencost-plugins/pkg/common => ../../common
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hashicorp/go-plugin v1.6.0
|
||||
github.com/opencost/opencost-plugins/pkg/common v0.0.0-00010101000000-000000000000
|
||||
github.com/opencost/opencost/core v0.0.0-20240307141548-816f98c9051a
|
||||
golang.org/x/time v0.5.0
|
||||
google.golang.org/protobuf v1.33.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // 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/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-hclog v1.6.2 // 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.7 // 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/v2 v2.1.1 // indirect
|
||||
github.com/rs/zerolog v1.32.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.18.2 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
|
||||
golang.org/x/net v0.29.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240304212257-790db918fca8 // indirect
|
||||
google.golang.org/grpc v1.62.1 // 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.29.2 // indirect
|
||||
k8s.io/apimachinery v0.29.2 // indirect
|
||||
k8s.io/klog/v2 v2.120.1 // indirect
|
||||
k8s.io/utils v0.0.0-20240102154912-e7106e64919e // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
|
||||
)
|
|
@ -0,0 +1,197 @@
|
|||
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/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/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/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-hclog v1.6.2 h1:NOtoftovWkDheyUM/8JW3QMiXyxJK3uHRK7wV04nD2I=
|
||||
github.com/hashicorp/go-hclog v1.6.2/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
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.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A=
|
||||
github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI=
|
||||
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/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
||||
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||
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.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
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/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
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.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.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
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/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/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 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
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-20240307141548-816f98c9051a h1:m6sesjHd7phuhoWhrCXrzLKHJbAdlH0Q07Uvpbgl4G0=
|
||||
github.com/opencost/opencost/core v0.0.0-20240307141548-816f98c9051a/go.mod h1:9o1Jfz3nuxVYRmlGk4xo84XZxoQk/LHqPd+Kvo1YIZ4=
|
||||
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/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
|
||||
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
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.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
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/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
||||
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/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
||||
golang.org/x/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-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
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/genproto/googleapis/rpc v0.0.0-20240304212257-790db918fca8 h1:IR+hp6ypxjH24bkMfEJ0yHR21+gwPWdV+/IBrPQyn3k=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240304212257-790db918fca8/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs=
|
||||
google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
|
||||
google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/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.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/api v0.29.2 h1:hBC7B9+MU+ptchxEqTNW2DkUosJpp1P+Wn6YncZ474A=
|
||||
k8s.io/api v0.29.2/go.mod h1:sdIaaKuU7P44aoyyLlikSLayT6Vb7bvJNCX105xZXY0=
|
||||
k8s.io/apimachinery v0.29.2 h1:EWGpfJ856oj11C52NRCHuU7rFDwxev48z+6DSlGNsV8=
|
||||
k8s.io/apimachinery v0.29.2/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU=
|
||||
k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
|
||||
k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ=
|
||||
k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
|
@ -0,0 +1,22 @@
|
|||
package openaiplugin
|
||||
|
||||
// OpenAIBilling represents the structure of the response JSON
|
||||
type OpenAIBilling struct {
|
||||
Object string `json:"object"`
|
||||
Data []BillingData `json:"data"`
|
||||
}
|
||||
|
||||
// BillingData represents the individual Billing data entries
|
||||
type BillingData struct {
|
||||
Timestamp float64 `json:"timestamp"`
|
||||
Currency string `json:"currency"`
|
||||
Name string `json:"name"`
|
||||
Cost float64 `json:"cost"`
|
||||
OrganizationID string `json:"organization_id"`
|
||||
ProjectID string `json:"project_id"`
|
||||
ProjectName string `json:"project_name"`
|
||||
OrganizationName string `json:"organization_name"`
|
||||
CostInMajorStr string `json:"cost_in_major"`
|
||||
CostInMajor float64 `json:"-"`
|
||||
Date string `json:"date"`
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package openaiplugin
|
||||
|
||||
type OpenAIConfig struct {
|
||||
APIKey string `json:"openai_api_key"`
|
||||
LogLevel string `json:"log_level"`
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package openaiplugin
|
||||
|
||||
type OpenAIUsage struct {
|
||||
Object string `json:"object"`
|
||||
Data []UsageData `json:"data"`
|
||||
}
|
||||
|
||||
type UsageData struct {
|
||||
OrganizationID string `json:"organization_id"`
|
||||
OrganizationName string `json:"organization_name"`
|
||||
AggregationTimestamp int `json:"aggregation_timestamp"`
|
||||
NRequests int `json:"n_requests"`
|
||||
Operation string `json:"operation"`
|
||||
SnapshotID string `json:"snapshot_id"`
|
||||
NContextTokensTotal int `json:"n_context_tokens_total"`
|
||||
NGeneratedTokensTotal int `json:"n_generated_tokens_total"`
|
||||
Email *string `json:"email"`
|
||||
APIKeyID *string `json:"api_key_id"`
|
||||
APIKeyName *string `json:"api_key_name"`
|
||||
APIKeyRedacted *string `json:"api_key_redacted"`
|
||||
APIKeyType *string `json:"api_key_type"`
|
||||
ProjectID *string `json:"project_id"`
|
||||
ProjectName *string `json:"project_name"`
|
||||
RequestType string `json:"request_type"`
|
||||
NCachedContextTokensTotal int `json:"n_cached_context_tokens_total"`
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
module github.com/opencost/opencost-plugins/test
|
||||
module github.com/opencost/opencost-plugins/pkg/test
|
||||
|
||||
go 1.21.6
|
||||
|
||||
|
@ -6,14 +6,18 @@ require (
|
|||
github.com/hashicorp/go-hclog v1.6.2
|
||||
github.com/hashicorp/go-plugin v1.6.0
|
||||
github.com/opencost/opencost/core v0.0.0-20240307141548-816f98c9051a
|
||||
github.com/spf13/cobra v1.8.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // 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/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
@ -1,6 +1,7 @@
|
|||
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/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
|
@ -17,14 +18,20 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
|
|||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
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/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-hclog v1.6.2 h1:NOtoftovWkDheyUM/8JW3QMiXyxJK3uHRK7wV04nD2I=
|
||||
github.com/hashicorp/go-hclog v1.6.2/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
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.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A=
|
||||
github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI=
|
||||
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/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
||||
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
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/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
|
@ -65,6 +72,7 @@ github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncj
|
|||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
|
||||
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
|
@ -75,6 +83,8 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
|||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
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.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
|
@ -0,0 +1,192 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
harness "github.com/opencost/opencost-plugins/pkg/test/pkg/harness"
|
||||
"github.com/opencost/opencost/core/pkg/log"
|
||||
"github.com/opencost/opencost/core/pkg/model/pb"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var plugins []string
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "plugin-harness",
|
||||
Short: "A test harness for opencost plugins",
|
||||
Long: `This program will invoke each plugin in turn, and will confirm no errors, and that the returned costs are non-zero.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
log.Info("running opencost plugin integration test harness")
|
||||
log.Info("this program will invoke each plugin in turn, and then will call a validator to confirm the results.")
|
||||
log.Info("it is up to plugin implementors to ensure that their plugins edge cases are covered by unit tests.")
|
||||
log.Info("this harness requires the JSON config for each plugin to be present in secret env vars")
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalf("error getting current working directory: %s", err)
|
||||
}
|
||||
log.Infof("current working directory: %s", cwd)
|
||||
var validationErrors error
|
||||
|
||||
// for each plugin given via a flag
|
||||
for _, plugin := range plugins {
|
||||
log.Infof("Testing plugin: %s", plugin)
|
||||
plugin = strings.TrimSpace(plugin)
|
||||
if plugin == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// write the config in PLUGIN_NAME_CONFIG out to a file
|
||||
envVarName := fmt.Sprintf("%s_CONFIG", strings.ReplaceAll(strings.ToUpper(plugin), "-", "_"))
|
||||
config := os.Getenv(envVarName)
|
||||
if len(config) == 0 {
|
||||
log.Fatalf("missing config for plugin %s", plugin)
|
||||
}
|
||||
|
||||
// write the config to a file
|
||||
configDir := os.TempDir()
|
||||
|
||||
file, err := os.CreateTemp(configDir, fmt.Sprintf("%s_config.json", plugin))
|
||||
if err != nil {
|
||||
log.Fatalf("error creating temp file for plugin %s: %s", plugin, err)
|
||||
}
|
||||
defer os.RemoveAll(file.Name())
|
||||
|
||||
_, err = file.WriteString(config)
|
||||
if err != nil {
|
||||
log.Fatalf("error writing config for plugin %s: %s", plugin, err)
|
||||
}
|
||||
|
||||
// request usage for last week in daily increments
|
||||
windowStart := time.Now().AddDate(0, 0, -7).Truncate(24 * time.Hour)
|
||||
windowEnd := time.Now().Add(24 * time.Hour).Truncate(24 * time.Hour)
|
||||
// invoke plugin via harness
|
||||
|
||||
pluginPath := cwd + "/pkg/plugins/" + plugin
|
||||
respDaily := getResponse(pluginPath, file.Name(), windowStart, windowEnd, 24*time.Hour)
|
||||
|
||||
// request usage for 3 days ago in hourly increments
|
||||
windowStart = time.Now().AddDate(0, 0, -4).Truncate(24 * time.Hour)
|
||||
windowEnd = time.Now().AddDate(0, 0, -3).Truncate(24 * time.Hour)
|
||||
// invoke plugin via harness
|
||||
respHourly := getResponse(pluginPath, file.Name(), windowStart, windowEnd, 1*time.Hour)
|
||||
|
||||
// call validator if implemented
|
||||
validator := validatorPath(pluginPath)
|
||||
if validator != "" {
|
||||
// write hourly cost response to a file
|
||||
hourlyBytes, err := marshal(respHourly)
|
||||
if err != nil {
|
||||
log.Fatalf("error marshalling hourly response for plugin %s: %s", plugin, err)
|
||||
}
|
||||
hourlyFile, err := os.CreateTemp("", fmt.Sprintf("%s_hourly_response_*.pb", plugin))
|
||||
if err != nil {
|
||||
log.Fatalf("error creating temp file for hourly response for plugin %s: %s", plugin, err)
|
||||
}
|
||||
|
||||
_, err = hourlyFile.Write(hourlyBytes)
|
||||
if err != nil {
|
||||
log.Fatalf("error writing hourly response for plugin %s: %s", plugin, err)
|
||||
}
|
||||
|
||||
// write daily cost response to a file
|
||||
dailyBytes, err := marshal(respDaily)
|
||||
if err != nil {
|
||||
log.Fatalf("error marshalling daily response for plugin %s: %s", plugin, err)
|
||||
}
|
||||
dailyFile, err := os.CreateTemp("", fmt.Sprintf("%s_daily_response_*.pb", plugin))
|
||||
if err != nil {
|
||||
log.Fatalf("error creating temp file for daily response for plugin %s: %s", plugin, err)
|
||||
}
|
||||
|
||||
_, err = dailyFile.Write(dailyBytes)
|
||||
if err != nil {
|
||||
log.Fatalf("error writing daily response for plugin %s: %s", plugin, err)
|
||||
}
|
||||
|
||||
err = invokeValidator(validator, hourlyFile.Name(), dailyFile.Name())
|
||||
if err != nil {
|
||||
validationErrors = multierror.Append(validationErrors, fmt.Errorf("error testing plugin %s: %w", plugin, err))
|
||||
}
|
||||
|
||||
} else {
|
||||
log.Infof("no validator found for plugin %s. Consider implementing a validator to improve the quality of the integration tests", plugin)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if validationErrors != nil {
|
||||
log.Fatalf("TESTS FAILED - validation errors: %s", validationErrors)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
rootCmd.Flags().StringSliceVarP(&plugins, "plugins", "p", []string{}, "List of plugins to test (comma-separated)")
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func invokeValidator(validatorPath, hourlyPath, dailyPath string) error {
|
||||
// invoke validator
|
||||
|
||||
// Create the command with the given arguments
|
||||
cmd := exec.Command("go", "run", validatorPath, dailyPath, hourlyPath)
|
||||
|
||||
// Run the command and capture the output
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Errorf("error running validator command: %s\nOutput: %s", err, output)
|
||||
return fmt.Errorf("error running validator command: %s, output: %s", err, output)
|
||||
}
|
||||
|
||||
// Print the output of the command
|
||||
fmt.Printf("Validator output:\n%s\n", output)
|
||||
return nil
|
||||
}
|
||||
|
||||
func marshal(protoResps []*pb.CustomCostResponse) ([]byte, error) {
|
||||
raw := make([]json.RawMessage, len(protoResps))
|
||||
for i, p := range protoResps {
|
||||
r, err := protojson.Marshal(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw[i] = r
|
||||
}
|
||||
|
||||
return json.Marshal(raw)
|
||||
}
|
||||
|
||||
func validatorPath(plugin string) string {
|
||||
path := plugin + "/cmd/validator/main/main.go"
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return ""
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func getResponse(pluginPath, pathToConfigFile string, windowStart, windowEnd time.Time, step time.Duration) []*pb.CustomCostResponse {
|
||||
|
||||
// invoke plugin via harness
|
||||
pluginFile := pluginPath + "/cmd/main/main.go"
|
||||
|
||||
req := pb.CustomCostRequest{
|
||||
Start: timestamppb.New(windowStart),
|
||||
End: timestamppb.New(windowEnd),
|
||||
Resolution: durationpb.New(step),
|
||||
}
|
||||
return harness.InvokePlugin(pathToConfigFile, pluginFile, &req)
|
||||
}
|
|
@ -26,26 +26,26 @@ fi
|
|||
echo $(pwd)
|
||||
plugin_name=$(basename $(dirname $gomod))
|
||||
echo "plugin name: $plugin_name"
|
||||
cd $plugin_name
|
||||
GOOS=linux GOARCH=arm64 go build -x -o "../build/$plugin_name.ocplugin.linux.arm64" \
|
||||
cd pkg/plugins/$plugin_name
|
||||
GOOS=linux GOARCH=arm64 go build -o "../../../build/$plugin_name.ocplugin.linux.arm64" \
|
||||
-ldflags \
|
||||
"-X github.com/opencost/opencost/pkg/version.Version=$VERSION \
|
||||
-X github.com/opencost/opencost/pkg/version.GitCommit=$COMMIT" \
|
||||
./cmd/main
|
||||
|
||||
GOOS=linux GOARCH=amd64 go build -x -o "../build/$plugin_name.ocplugin.linux.amd64" \
|
||||
GOOS=linux GOARCH=amd64 go build -o "../../../build/$plugin_name.ocplugin.linux.amd64" \
|
||||
-ldflags \
|
||||
"-X github.com/opencost/opencost/pkg/version.Version=$VERSION \
|
||||
-X github.com/opencost/opencost/pkg/version.GitCommit=$COMMIT" \
|
||||
./cmd/main
|
||||
|
||||
GOOS=darwin GOARCH=arm64 go build -x -o "../build/$plugin_name.ocplugin.darwin.arm64" \
|
||||
GOOS=darwin GOARCH=arm64 go build -o "../../../build/$plugin_name.ocplugin.darwin.arm64" \
|
||||
-ldflags \
|
||||
"-X github.com/opencost/opencost/pkg/version.Version=$VERSION \
|
||||
-X github.com/opencost/opencost/pkg/version.GitCommit=$COMMIT" \
|
||||
./cmd/main
|
||||
|
||||
GOOS=darwin GOARCH=amd64 go build -x -o "../build/$plugin_name.ocplugin.darwin.amd64" \
|
||||
GOOS=darwin GOARCH=amd64 go build -o "../../../build/$plugin_name.ocplugin.darwin.amd64" \
|
||||
-ldflags \
|
||||
"-X github.com/opencost/opencost/pkg/version.Version=$VERSION \
|
||||
-X github.com/opencost/opencost/pkg/version.GitCommit=$COMMIT" \
|
||||
|
|
Loading…
Reference in New Issue