diff --git a/Makefile b/Makefile index f5cecdf..cc00ec2 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,7 @@ ensure-prometheus: .cache/prometheus ## Ensures that Prometheus is installed in .PHONY: test test: export ACR_DB_URL = postgres://user:password@localhost:55432/db?sslmode=disable -test: +test: ensure-prometheus go test ./... -tags integration -coverprofile cover.out -covermode atomic .PHONY: fmt diff --git a/Makefile.vars.mk b/Makefile.vars.mk index 682c69e..b7060d9 100644 --- a/Makefile.vars.mk +++ b/Makefile.vars.mk @@ -14,11 +14,6 @@ IMG_TAG ?= latest # Image URL to use all building/pushing image targets CONTAINER_IMG ?= local.dev/$(PROJECT_OWNER)/$(PROJECT_NAME):$(IMG_TAG) -## COMPOSE: -COMPOSE_CMD ?= docker-compose -COMPOSE_DB_URL ?= postgres://reporting:reporting@localhost:55432/reporting-db?sslmode=disable -COMPOSE_FILE ?= docker-compose.yml - PROMETHEUS_VERSION ?= 2.40.7 PROMETHEUS_DIST ?= $(shell go env GOOS) PROMETHEUS_ARCH ?= $(shell go env GOARCH) diff --git a/go.mod b/go.mod index 7315656..2e2e549 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/prometheus/common v0.40.0 github.com/stretchr/testify v1.8.2 github.com/urfave/cli/v2 v2.24.4 + go.uber.org/multierr v1.6.0 go.uber.org/zap v1.24.0 ) @@ -23,6 +24,7 @@ require ( github.com/benbjohnson/clock v1.1.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect @@ -36,9 +38,12 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.uber.org/atomic v1.7.0 // indirect - go.uber.org/multierr v1.6.0 // indirect - golang.org/x/crypto v0.6.0 // indirect - golang.org/x/text v0.7.0 // indirect + golang.org/x/crypto v0.15.0 // indirect + golang.org/x/net v0.18.0 // indirect + golang.org/x/oauth2 v0.14.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect sigs.k8s.io/yaml v1.1.0 // indirect diff --git a/go.sum b/go.sum index 733e43f..f2477aa 100644 --- a/go.sum +++ b/go.sum @@ -28,7 +28,12 @@ github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-jsonnet v0.19.1 h1:MORxkrG0elylUqh36R4AcSPX0oZQa9hvI3lroN+kDhs= github.com/google/go-jsonnet v0.19.1/go.mod h1:5JVT33JVCoehdTj5Z2KJq1eIdt3Nb8PCmZ+W5D8U350= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -198,19 +203,25 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 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-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-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= +golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/oauth2 v0.3.0 h1:6l90koy8/LaBLmLu8jpHeHexzMwEita0zFfYlggy2F8= +golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0= +golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -242,6 +253,7 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -257,9 +269,15 @@ golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +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.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 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-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/odoo/odoo16.go b/pkg/odoo/odoo16.go index 44b6b72..e788aef 100644 --- a/pkg/odoo/odoo16.go +++ b/pkg/odoo/odoo16.go @@ -1,17 +1,26 @@ package odoo import ( + "bytes" "context" "encoding/json" + "errors" + "fmt" + "io" + "net/http" "github.com/go-logr/logr" + "golang.org/x/oauth2/clientcredentials" ) type OdooAPIClient struct { - OdooURL string - OauthClientID string - OauthClientSecret string - logger logr.Logger + odooURL string + logger logr.Logger + oauthClient *http.Client +} + +type apiObject struct { + Data []OdooMeteredBillingRecord `json:"data"` } type OdooMeteredBillingRecord struct { @@ -25,18 +34,44 @@ type OdooMeteredBillingRecord struct { Timerange string `json:"timerange"` } -func NewOdooAPIClient(odooURL string, oauthClientId string, oauthClientSecret string, logger logr.Logger) (*OdooAPIClient, error) { +func NewOdooAPIClient(ctx context.Context, odooURL string, oauthTokenURL string, oauthClientId string, oauthClientSecret string, logger logr.Logger) *OdooAPIClient { + oauthConfig := clientcredentials.Config{ + ClientID: oauthClientId, + ClientSecret: oauthClientSecret, + TokenURL: oauthTokenURL, + } + oauthClient := oauthConfig.Client(ctx) return &OdooAPIClient{ - OdooURL: odooURL, - OauthClientID: oauthClientId, - OauthClientSecret: oauthClientSecret, - logger: logger, - }, nil + odooURL: odooURL, + logger: logger, + oauthClient: oauthClient, + } } -func (c OdooAPIClient) SendData(ctx context.Context, data OdooMeteredBillingRecord) error { - str, _ := json.Marshal(data) - c.logger.Info("<" + data.InstanceID + ">") - c.logger.Info(string(str)) +func NewOdooAPIWithClient(odooURL string, client *http.Client, logger logr.Logger) *OdooAPIClient { + return &OdooAPIClient{ + odooURL: odooURL, + logger: logger, + oauthClient: client, + } +} + +func (c OdooAPIClient) SendData(ctx context.Context, data []OdooMeteredBillingRecord) error { + apiObject := apiObject{ + Data: data, + } + str, _ := json.Marshal(apiObject) + resp, err := c.oauthClient.Post(c.odooURL, "application/json", bytes.NewBuffer(str)) + if err != nil { + return err + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + c.logger.Info("Records sent to Odoo API", "status", resp.Status, "body", string(body), "numberOfRecords", len(data)) + + if resp.StatusCode/100 != 2 { + return errors.New(fmt.Sprintf("API error when sending records to Odoo:\n%s", body)) + } + return nil } diff --git a/pkg/odoo/odoo16_test.go b/pkg/odoo/odoo16_test.go new file mode 100644 index 0000000..b724421 --- /dev/null +++ b/pkg/odoo/odoo16_test.go @@ -0,0 +1,126 @@ +package odoo_test + +import ( + "context" + "errors" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-logr/logr" + "github.com/stretchr/testify/require" + + "github.com/appuio/appuio-cloud-reporting/pkg/odoo" +) + +type mockRoundTripper struct { + cannedResponse *http.Response + receivedContent string +} + +type mockRoundTripperWhichFails struct { +} + +func (rt *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + body, _ := io.ReadAll(req.Body) + rt.receivedContent = string(body) + return rt.cannedResponse, nil +} + +func (rt *mockRoundTripperWhichFails) RoundTrip(req *http.Request) (*http.Response, error) { + return nil, errors.New("there was an error") +} + +func TestOdooRecordsSent(t *testing.T) { + recorder := httptest.NewRecorder() + recorder.WriteString("success") + expectedResponse := recorder.Result() + + mrt := &mockRoundTripper{cannedResponse: expectedResponse} + client := http.Client{Transport: mrt} + + logger := logr.New(logr.Discard().GetSink()) + uut := odoo.NewOdooAPIWithClient("https://foo.bar/odoo16/", &client, logger) + + err := uut.SendData(context.Background(), []odoo.OdooMeteredBillingRecord{getOdooRecord()}) + + require.NoError(t, err) + require.Equal(t, mrt.receivedContent, `{"data":[{"product_id":"my-product","instance_id":"my-instance","item_description":"my-description","item_group_description":"my-group","sales_order_id":"SO00000","unit_id":"my-unit","consumed_units":11.1,"timerange":"my-timerange"}]}`) +} + +func TestErrorHandling(t *testing.T) { + mrt := &mockRoundTripperWhichFails{} + client := http.Client{Transport: mrt} + + logger := logr.New(logr.Discard().GetSink()) + uut := odoo.NewOdooAPIWithClient("https://foo.bar/odoo16/", &client, logger) + + err := uut.SendData(context.Background(), []odoo.OdooMeteredBillingRecord{getOdooRecord()}) + + require.Error(t, err) +} + +func TestErrorFromServerRaisesError(t *testing.T) { + recorder := httptest.NewRecorder() + recorder.WriteHeader(500) + recorder.WriteString(`{ + "arguments": [ + "data" + ], + "code": 500, + "context": {}, + "message": "data", + "name": "builtins.KeyError", + "traceback": [ + "Traceback (most recent call last):", + " File \"/opt/odoo/bin/odoo/http.py\", line 1589, in _serve_db", + " return service_model.retrying(self._serve_ir_http, self.env)", + " File \"/opt/odoo/bin/odoo/service/model.py\", line 133, in retrying", + " result = func()", + " File \"/opt/odoo/bin/odoo/http.py\", line 1616, in _serve_ir_http", + " response = self.dispatcher.dispatch(rule.endpoint, args)", + " File \"/opt/odoo/braintec/ext/muk_rest/core/http.py\", line 295, in dispatch", + " result = self.request.registry['ir.http']._dispatch(endpoint)", + " File \"/opt/odoo/bin/addons/website/models/ir_http.py\", line 237, in _dispatch", + " response = super()._dispatch(endpoint)", + " File \"/opt/odoo/braintec/ext/muk_rest/models/ir_http.py\", line 160, in _dispatch", + " response = super()._dispatch(endpoint)", + " File \"/opt/odoo/addons/monitoring_prometheus/models/ir_http.py\", line 38, in _dispatch", + " res = super()._dispatch(endpoint)", + " File \"/opt/odoo/bin/odoo/addons/base/models/ir_http.py\", line 154, in _dispatch", + " result = endpoint(**request.params)", + " File \"/opt/odoo/bin/odoo/http.py\", line 697, in route_wrapper", + " result = endpoint(self, *args, **params_ok)", + " File \"/opt/odoo/braintec/ext/muk_rest/core/http.py\", line 122, in wrapper", + " result = func(*args, **kwargs)", + " File \"/opt/odoo/braintec/vshn/vshn_metered_usage_rest/controllers/metered_usage_rest.py\", line 65, in vshn_send_metered_usage", + " 'payload': json.dumps(kw['data'], indent=4)", + "KeyError: 'data'" + ] +}`) + expectedResponse := recorder.Result() + + mrt := &mockRoundTripper{cannedResponse: expectedResponse} + client := http.Client{Transport: mrt} + + logger := logr.New(logr.Discard().GetSink()) + uut := odoo.NewOdooAPIWithClient("https://foo.bar/odoo16/", &client, logger) + + err := uut.SendData(context.Background(), []odoo.OdooMeteredBillingRecord{getOdooRecord()}) + + require.Error(t, err) +} + +func getOdooRecord() odoo.OdooMeteredBillingRecord { + return odoo.OdooMeteredBillingRecord{ + ProductID: "my-product", + UnitID: "my-unit", + SalesOrderID: "SO00000", + InstanceID: "my-instance", + ItemDescription: "my-description", + ItemGroupDescription: "my-group", + ConsumedUnits: 11.1, + Timerange: "my-timerange", + } +} diff --git a/pkg/report/report.go b/pkg/report/report.go index 867b130..5921de5 100644 --- a/pkg/report/report.go +++ b/pkg/report/report.go @@ -20,7 +20,7 @@ type PromQuerier interface { } type OdooClient interface { - SendData(ctx context.Context, data odoo.OdooMeteredBillingRecord) error + SendData(ctx context.Context, data []odoo.OdooMeteredBillingRecord) error } type ReportArgs struct { @@ -71,7 +71,7 @@ func Run(ctx context.Context, odoo OdooClient, prom PromQuerier, args ReportArgs return nil } -func runQuery(ctx context.Context, odoo OdooClient, prom PromQuerier, args ReportArgs, from time.Time, opts options) error { +func runQuery(ctx context.Context, odooClient OdooClient, prom PromQuerier, args ReportArgs, from time.Time, opts options) error { promQCtx := ctx if opts.prometheusQueryTimeout != 0 { ctx, cancel := context.WithTimeout(promQCtx, opts.prometheusQueryTimeout) @@ -91,54 +91,57 @@ func runQuery(ctx context.Context, odoo OdooClient, prom PromQuerier, args Repor } var errs error + var records []odoo.OdooMeteredBillingRecord for _, sample := range samples { - if err := processSample(ctx, odoo, args, from, sample); err != nil { - + record, err := processSample(ctx, odooClient, args, from, sample) + if err != nil { errs = multierr.Append(errs, fmt.Errorf("failed to process sample: %w", err)) + } else { + records = append(records, *record) } } - return errs + return multierr.Append(errs, odooClient.SendData(ctx, records)) } -func processSample(ctx context.Context, odooClient OdooClient, args ReportArgs, from time.Time, s *model.Sample) error { +func processSample(ctx context.Context, odooClient OdooClient, args ReportArgs, from time.Time, s *model.Sample) (*odoo.OdooMeteredBillingRecord, error) { variables := extractTemplateVars(args) values := make(map[string]string) for i := 0; i < len(variables); i++ { value, err := getMetricLabel(s.Metric, variables[i]) if err != nil { - return fmt.Errorf("Unable to obtain sales order ID from sample: %w", err) + return nil, fmt.Errorf("Unable to obtain label %s from sample: %w", variables[i], err) } values[variables[i]] = string(value) } salesOrderID, err := getMetricLabel(s.Metric, SalesOrderIDLabel) if err != nil { - return err + return nil, err } jsonStr, err := json.Marshal(values) if err != nil { - return err + return nil, err } vm := jsonnet.MakeVM() instance, err := vm.EvaluateAnonymousSnippet("snip.json", fmt.Sprintf("\"%s\" %% %s", args.InstancePattern, jsonStr)) if err != nil { - return err + return nil, err } instance = strings.Trim(instance, "\"\n") group, err := vm.EvaluateAnonymousSnippet("snip.json", fmt.Sprintf("\"%s\" %% %s", args.ItemGroupDescriptionPattern, jsonStr)) if err != nil { - return err + return nil, err } group = strings.Trim(group, "\"\n") description, err := vm.EvaluateAnonymousSnippet("snip.json", fmt.Sprintf("\"%s\" %% %s", args.ItemDescriptionPattern, jsonStr)) if err != nil { - return err + return nil, err } description = strings.Trim(description, "\"\n") @@ -155,7 +158,7 @@ func processSample(ctx context.Context, odooClient OdooClient, args ReportArgs, Timerange: timerange, } - return odooClient.SendData(ctx, record) + return &record, nil } func extractTemplateVars(args ReportArgs) []string { diff --git a/pkg/report/report_test.go b/pkg/report/report_test.go index 8ae41f0..7002a16 100644 --- a/pkg/report/report_test.go +++ b/pkg/report/report_test.go @@ -105,11 +105,11 @@ func (s *ReportSuite) TestReport_Run() { err := report.Run(context.Background(), o, prom, args, from) require.NoError(t, err) - require.Equal(t, "my-namespace", o.lastReceivedData.ItemGroupDescription) - require.Equal(t, "my-tenant", o.lastReceivedData.InstanceID) - require.Equal(t, "my-product", o.lastReceivedData.ItemDescription) - require.Equal(t, 1.0, o.lastReceivedData.ConsumedUnits) - require.Equal(t, "SO00000", o.lastReceivedData.SalesOrderID) + require.Equal(t, "my-namespace", o.lastReceivedData[0].ItemGroupDescription) + require.Equal(t, "my-tenant", o.lastReceivedData[0].InstanceID) + require.Equal(t, "my-product", o.lastReceivedData[0].ItemDescription) + require.Equal(t, 1.0, o.lastReceivedData[0].ConsumedUnits) + require.Equal(t, "SO00000", o.lastReceivedData[0].SalesOrderID) } func (s *ReportSuite) TestReport_RequireErrorWhenInvalidTemplateVariable() { @@ -164,11 +164,11 @@ func getReportArgs() report.ReportArgs { type MockOdooClient struct { totalReceived int - lastReceivedData odoo.OdooMeteredBillingRecord + lastReceivedData []odoo.OdooMeteredBillingRecord } -func (c *MockOdooClient) SendData(ctx context.Context, data odoo.OdooMeteredBillingRecord) error { +func (c *MockOdooClient) SendData(ctx context.Context, data []odoo.OdooMeteredBillingRecord) error { c.lastReceivedData = data - c.totalReceived = c.totalReceived + 1 + c.totalReceived += 1 return nil } diff --git a/report_command.go b/report_command.go index f661f60..219e71d 100644 --- a/report_command.go +++ b/report_command.go @@ -15,10 +15,11 @@ import ( ) type reportCommand struct { - PrometheusURL string - OdooURL string - OdooClientId string - OdooClientSecret string + PrometheusURL string + OdooURL string + OdooOauthTokenURL string + OdooClientId string + OdooClientSecret string ReportArgs report.ReportArgs @@ -44,6 +45,8 @@ func newReportCommand() *cli.Command { EnvVars: envVars("PROM_URL"), Destination: &command.PrometheusURL, Value: "http://localhost:9090"}, &cli.StringFlag{Name: "odoo-url", Usage: "URL of the Odoo Metered Billing API", EnvVars: envVars("ODOO_URL"), Destination: &command.OdooURL, Value: "http://localhost:8080"}, + &cli.StringFlag{Name: "odoo-oauth-token-url", Usage: "Oauth Token URL to authenticate with Odoo metered billing API", + EnvVars: envVars("ODOO_OAUTH_TOKEN_URL"), Destination: &command.OdooOauthTokenURL, Required: true, DefaultText: defaultTextForRequiredFlags}, &cli.StringFlag{Name: "odoo-oauth-client-id", Usage: "Client ID of the oauth client to interact with Odoo metered billing API", EnvVars: envVars("ODOO_OAUTH_CLIENT_ID"), Destination: &command.OdooClientId, Required: true, DefaultText: defaultTextForRequiredFlags}, &cli.StringFlag{Name: "odoo-oauth-client-secret", Usage: "Client secret of the oauth client to interact with Odoo metered billing API", @@ -91,10 +94,7 @@ func (cmd *reportCommand) execute(cliCtx *cli.Context) error { return fmt.Errorf("could not create prometheus client: %w", err) } - odooClient, err := odoo.NewOdooAPIClient(cmd.OdooURL, cmd.OdooClientId, cmd.OdooClientSecret, log) - if err != nil { - return fmt.Errorf("could not create odoo client: %w", err) - } + odooClient := odoo.NewOdooAPIClient(ctx, cmd.OdooURL, cmd.OdooOauthTokenURL, cmd.OdooClientId, cmd.OdooClientSecret, log) o := make([]report.Option, 0) if cmd.PromQueryTimeout != 0 {