From 8f1f7ad6eaf8075a867e3131fb79a75c9ae2f79f Mon Sep 17 00:00:00 2001 From: Huy Huynh Date: Thu, 16 Jan 2020 13:37:47 -0800 Subject: [PATCH] Sample application for sending a test report --- entitlement.go | 205 +++++++++++++++++++++++++++++++++++++ fake_reporting_secret.yaml | 7 ++ main.go | 112 -------------------- 3 files changed, 212 insertions(+), 112 deletions(-) create mode 100644 entitlement.go create mode 100644 fake_reporting_secret.yaml delete mode 100644 main.go diff --git a/entitlement.go b/entitlement.go new file mode 100644 index 0000000..912c90f --- /dev/null +++ b/entitlement.go @@ -0,0 +1,205 @@ +package main + +// +// Installation instruction (golang tools are required): +// $ make setup +// $ make deps +// $ make build +// +// Sample command: +// $ bin/ubbagent -logtostderr \ +// -reporting-secret fake_reporting_secret.yaml \ +// -service-name your-solution.your-service-id.appspot.com +// -metric-name your-metric-name \ +// -metric-int-value 1 +// +// The service name and metric name are configured when billing +// is setup for your listing. +// +// If you deploy Cloud Bees Core Billable solution from Marketplace +// and obtain its reporting secret (look for *-reporting-secret Secret +// in the target namespace), you can use the following: +// $ bin/ubbagent -logtostderr \ +// -reporting-secret reporting_secret.yaml \ +// -service-name cloudbees-core-billable.mp-cloudbees.appspot.com \ +// -metric-name user \ +// -metric-int-value 1 +// + +import ( + "context" + "encoding/base64" + "errors" + "flag" + "fmt" + "io/ioutil" + "os" + "time" + + "github.com/GoogleCloudPlatform/ubbagent/config" + "golang.org/x/oauth2/google" + "google.golang.org/api/servicecontrol/v1" + + "github.com/ghodss/yaml" + "github.com/golang/glog" + "github.com/google/uuid" +) + +var secretPath = flag.String("reporting-secret", "", "K8s reporting secret YAML") +var serviceName = flag.String("service-name", "", "Service name") +var metricName = flag.String("metric-name", "", "Metric name") +var metricValue = flag.Int64("metric-int-value", 1, "Metric int64 value") + +type secret struct { + Data secretData `json:"data"` +} + +type secretData struct { + ConsumerID consumerID `json:"consumer-id"` + EntitlementID entitlementID `json:"entitlement-id"` + ReportingKey reportingKey `json:"reporting-key"` +} + +type consumerID string + +func (c *consumerID) UnmarshalJSON(data []byte) error { + decoded, err := decodeBase64Encoded(data) + if err != nil { + return err + } + *c = consumerID(decoded) + return nil +} + +type entitlementID string + +func (c *entitlementID) UnmarshalJSON(data []byte) error { + decoded, err := decodeBase64Encoded(data) + if err != nil { + return err + } + *c = entitlementID(decoded) + return nil +} + +type reportingKey config.EncodedServiceAccountKey + +func (c *reportingKey) UnmarshalJSON(data []byte) error { + decoded, err := decodeBase64Encoded(data) + if err != nil { + return err + } + if decoded == nil { + return err + } + accountKey := config.EncodedServiceAccountKey{} + accountKey.UnmarshalJSON(decoded) + *c = reportingKey(accountKey) + return nil +} + +func decodeBase64Encoded(data []byte) ([]byte, error) { + var decoded []byte + var encodedStr string + + // First we decode the data into a string to get rid of any start and end quotes. + err := yaml.Unmarshal(data, &encodedStr) + if err != nil { + return nil, errors.New("not a string value") + } + + decoded, err = base64.StdEncoding.DecodeString(encodedStr) + if err != nil { + return nil, errors.New("not a valid base64 value") + } + + return decoded, nil +} + +func main() { + flag.Parse() + + if *secretPath == "" || *serviceName == "" || *metricName == "" { + fmt.Fprintln(os.Stderr, "Required flags must be specified") + flag.Usage() + os.Exit(2) + } + reportingSecret, err := load(*secretPath) + check(err) + glog.Infof("ReportingSecret=%+v", reportingSecret) + service, err := newServiceControl(reportingSecret.Data.ReportingKey) + check(err) + + opID, err := uuid.NewRandom() + check(err) + op := &servicecontrol.Operation{ + OperationId: opID.String(), + // ServiceControl requires this field but doesn't indicate what it's supposed to be. + OperationName: fmt.Sprintf("%v/report", *serviceName), + StartTime: time.Now().Format(time.RFC3339Nano), + EndTime: time.Now().Format(time.RFC3339Nano), + ConsumerId: string(reportingSecret.Data.ConsumerID), + MetricValueSets: []*servicecontrol.MetricValueSet{ + { + MetricName: fmt.Sprintf("%v/%v", *serviceName, *metricName), + MetricValues: []*servicecontrol.MetricValue{ + &servicecontrol.MetricValue{ + StartTime: time.Now().Format(time.RFC3339Nano), + EndTime: time.Now().Format(time.RFC3339Nano), + Int64Value: &[]int64{*metricValue}[0], + }, + }, + }, + }, + } + req := &servicecontrol.ReportRequest{ + Operations: []*servicecontrol.Operation{op}, + } + response, err := service.Services.Report(*serviceName, req).Do() + glog.Infof("Response=%v\nError=%v", response, err) + if err == nil { + glog.Infof("SUCCESS!!!") + } else { + glog.Fatalf("DID NOT SUCCEED") + } +} + +func check(err error) { + if err != nil { + glog.Fatalf("Error: %v", err) + } +} + +func newServiceControl(jsonKey []byte) (*servicecontrol.Service, error) { + config, err := google.JWTConfigFromJSON(jsonKey, servicecontrol.ServicecontrolScope) + if err != nil { + return nil, err + } + client := config.Client(context.Background()) + client.Timeout = 30 * time.Second + service, err := servicecontrol.New(client) + if err != nil { + return nil, err + } + return service, nil +} + +func load(path string) (*secret, error) { + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + secret, err := parse(data) + if err != nil { + return nil, err + } + return secret, nil +} + +func parse(data []byte) (*secret, error) { + c := &secret{} + if err := yaml.Unmarshal(data, c); err != nil { + return nil, err + } + return c, nil +} diff --git a/fake_reporting_secret.yaml b/fake_reporting_secret.yaml new file mode 100644 index 0000000..61707e1 --- /dev/null +++ b/fake_reporting_secret.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +data: + consumer-id: cHJvamVjdDpwci14eHh4LWZha2UteHh4eA== + entitlement-id: ZmZmZmZmZmYtZmZmZi1mZmZmLWZmZmYtZmZmZmZmZmZmZmZm + reporting-key: ZXdvZ0lDSjBlWEJsSWpvZ0luTmxjblpwWTJWZllXTmpiM1Z1ZENJc0NpQWdJbkJ5YjJwbFkzUmZhV1FpT2lBaVkyeHZkV1F0YldGeWEyVjBjR3hoWTJVdGRHOXZiSE1pTEFvZ0lDSndjbWwyWVhSbFgydGxlVjlwWkNJNklDSm1OR1ppTUdRMk16TmhaRFEzWWpFd1pUSmhORFJqTTJaak1HWmlZakEzTlRrNE56Z3lZMkpqSWl3S0lDQWljSEpwZG1GMFpWOXJaWGtpT2lBaUxTMHRMUzFDUlVkSlRpQlFVa2xXUVZSRklFdEZXUzB0TFMwdFhHNU5TVWxGZGtGSlFrRkVRVTVDWjJ0eGFHdHBSemwzTUVKQlVVVkdRVUZUUTBKTFdYZG5aMU5wUVdkRlFVRnZTVUpCVVVONk1HOWlWbXh0VmpBNE1GVm9YRzV1WTNoMGQyZHlUVTFKTm1kMEsyTmhObFJxZGs0dmJrNW5hREZ1ZGsxVU1YUjBObnB0WWpaQ01HSTBTazF2V0V4SE4ycGpMM0Y1T1dKTGEwdERPRkEyWEc1bFdESmpXbkExWkVoT1VsQkZVRXhVWXpGV1RuQldZM0JQTm1ReFVYUnNWbTlOWVdVelZtTXlNbEYzTXpCQ2FsTkhTMU12V0U5V2IyUnNUeXRRTHpZM1hHNXhNMUZhVDNwWmJqRTRNakEyT0ZkM0wwVmxUM0UzWW1GdlQzQmllVlJEUlZRMlJUY3JPRzhyYVUxRVQxSklaVzVyTVRSck1rWjZSMjR6SzJOcU9VWklYRzVUTkhGQ0wxZFNLelF4Wm01bE5XOTFOVTVKU1hFMGFXVllWV2RsTnpaNk9GWXhaRE4xZFdGNWNGaGhZME50VEN0TVpGVnlZWFkxYjBSSU4zbEVTVUo2WEc1TWFHRkdTMjlsU0dKNlJGcFRkR2RTYVVKcFZqWkZielpoVFd4dU4wTndUR3hxYlZOUGRHb3JiVk5TYkhZemRFVTJibFU1TjFacU9FcHRhV05DUTNwV1hHNUpkRGx1TkhSelNFRm5UVUpCUVVWRFoyZEZRVUpTYm5Gd05rbHdlRVpKVW5WMFNUbEJUMWxhWlZaRldDOVhUMHRNYkZSQ2RpdFBUR1JvTWtRdmJEWk9YRzVaU1VwemRXVnhXbVJ6VWxVNVducFdlbE5sTDBGaFNIZHJOQzlEZGtsblRqWm5kakZSTTBWVU1FRlRPVXg1UW1Ka1l6QnlPVk53ZG1KdFZHRkJUWEJ6WEc1b2ExaFhaMlpQVUV4R1MyMDVWRGhMTTFSaWRYZFpXbFJUWW1sR2JsWkhUa2R3VkZOMmJqVXJjMFV5Y1dOU1dEVk1aV0Z1YkVaeE0wTkNTbXhxYTFOc1hHNWFZMDhyVW5ORVVqUlFOREJGVVdVd2FXUXhhek5UWVc5Sk1IUkJTMHRhTkdOQ2JHUmhTMUpCVkZSWE5IQnpOa2R2U0c5eU9WSTJOREJZWTFaUlN6bFdYRzVsVGxKUGFtTXdUbWx0Um5sS1VXWlVTMk5YYVV4SU1WaGlRVFJVUTI5R1YwaE9kVU5sTDJoV1VWUlNWbkpTT1hWb01YVlVTbkI1VUc0MFFrTXpabnBTWEc1TmRIRnlOWGxoVEc1eWJUTXlZMFFyYjBZclZuWllOMUpsVm10TVpuWk1TazVEVlVSVE9FRjFjVkZMUW1kUlJEQTROMlJIVUZKaFRXSkdPVVY1Ym1WcVhHNDVTM1JxU2xOQmNsaHpZbUp3YmxKUVJ6bFNhMEZVU1RsR2FYQnRURnBhY3pSVGFEYzVPV2RZTlZkUE5YUlZObGRCWVZWcVRVUjFNM3B6VEdaQ05DdFBYRzVOTkU5TVFtbDBMM2t3VnpGQlpsTTBUVVkyVG1wbWNFbGFTVWxzT1drMVowSkxVbkJGUVhCMGRtVnRTVzlHWkZsUlkweFRiMkYwU0ZKbGNIQmxOMmxpWEc1aU1IRm5ZaTlVVm5ORllYaGhaMUIyS3poSU4zcEVjVVpYVVV0Q1oxRkROemR6TlVnMFVqTjFXRFJVUzNKUk9XVTBTVkZOTm05cVVFSXhRazlFV0cxalhHNXJlVEpDVUc1TGNsTkJiMWN5Y2xsWlJYcG5OWHBGUW1weWVuZHNPVTFuSzBsaFFYVnViUzl5ZEVORlowbzJTMlpLVlhkWVEzaFdjVFpqVjIxeVZIaENYRzVoUTJaSFRFb3phaTg1YWxoTlEydEhUMnRVWVVoVWRqbGxjSFV3YkM5MFp6TTVOa3hDU1V4TWFXaG5PVzk2VVZvemJIbHJhemRIT1hoWWNtRkViVzVuWEc1S1RFUTVhM05rTTFoM1MwSm5SV054TDBObVJFaGxZMFZ2V2xaaFFXWkxNelZ2WlhsNFMzSXhTMWNyZERaQlRVbEJaV2huVmtwc1l6bEZjV3h0YUhabFhHNVBlVmg0WkRJMVVqZHRlVXBYWlVSS1lqVktUM1JFYzA5TWNEUm5SWHA0YzJsU05TdFdNbk5WZDNoaU5VUTBTRzlST0VJMk4wdHVWakJrTlROeVZHVnRYRzVuWVVsdmVpczNZMmsxY0habk5VVllOR2xRVTFwMlNWcFNVMk5MYkVST1RUTllSRXAwYldaVWFFZGhUbVE0Um1zNGVFcFhXSFphV2tGdlIwRlpVRTA0WEc1U1dXRlVNakZKTldab2EzcFZZakl2VVdFMlNXSXdNRlpzTXpaTFJ6QlZkRGt6ZGxGNWFESTJNM0pzY25OU1dGSk1jbVkxUXpkUWREaHhURW96YjNWWlhHNVRRbE5EU201V2FHeFhXRGxHWkRZeU1YcG9ibXA1VlZWVGRqQmFiR0ZDTW5wR2VHVkJOalJOU0dzNFFrTjFOWEZqU2pobFVVcEVUVE5MYUM5RVUxaFJYRzVrTmxWWVIwbDFaMGc0VVdVM05qRjJNalZOTlRSWE1rMURRWFpvWjB4c1RTdFVUMDFhVkRoRFoxbENjalY1UjFwaU1sQkdaaXRLYldabVRVMUVORTU2WEc1bGJFcDFTMUpsWXpSaWVscHNORkJHUTFjM09IcEpNaTlEWlRaak9IUm5XR04wT0VwdFRVTTJSMmt4YUVSQ01TOHZabFpJU2pjeFJubFNZakJJWVhWUFhHNVRXSFoyVlhRMU9HRXZMMUJzYzFaVWJHTnVjR0oyVUVWUlJUWTBTalo2VWpsbkswUkxOV2hFT0dvNFVVeFZhRmhrZEhGWVlqVmhVWEE0UXpsek5HNVBYRzVpY1VSUVozVm9MMHRKYVZBMVdTdGhVRTlsYUc1M1BUMWNiaTB0TFMwdFJVNUVJRkJTU1ZaQlZFVWdTMFZaTFMwdExTMWNiaUlzQ2lBZ0ltTnNhV1Z1ZEY5bGJXRnBiQ0k2SUNKNGVIZ3RabUZyWlMxeVpYQnZjblJsY2kxNGVIaEFZMnh2ZFdRdGJXRnlhMlYwY0d4aFkyVXRkRzl2YkhNdWFXRnRMbWR6WlhKMmFXTmxZV05qYjNWdWRDNWpiMjBpTEFvZ0lDSmpiR2xsYm5SZmFXUWlPaUFpTVRBMU9EWXlNRE0zT0RRMU1qazVOekkzT0RJeklpd0tJQ0FpWVhWMGFGOTFjbWtpT2lBaWFIUjBjSE02THk5aFkyTnZkVzUwY3k1bmIyOW5iR1V1WTI5dEwyOHZiMkYxZEdneUwyRjFkR2dpTEFvZ0lDSjBiMnRsYmw5MWNta2lPaUFpYUhSMGNITTZMeTl2WVhWMGFESXVaMjl2WjJ4bFlYQnBjeTVqYjIwdmRHOXJaVzRpTEFvZ0lDSmhkWFJvWDNCeWIzWnBaR1Z5WDNnMU1EbGZZMlZ5ZEY5MWNtd2lPaUFpYUhSMGNITTZMeTkzZDNjdVoyOXZaMnhsWVhCcGN5NWpiMjB2YjJGMWRHZ3lMM1l4TDJObGNuUnpJaXdLSUNBaVkyeHBaVzUwWDNnMU1EbGZZMlZ5ZEY5MWNtd2lPaUFpYUhSMGNITTZMeTkzZDNjdVoyOXZaMnhsWVhCcGN5NWpiMjB2Y205aWIzUXZkakV2YldWMFlXUmhkR0V2ZURVd09TOTRlSGd0Wm1GclpTMXlaWEJ2Y25SbGNpMTRlSGdsTkRCamJHOTFaQzF0WVhKclpYUndiR0ZqWlMxMGIyOXNjeTVwWVcwdVozTmxjblpwWTJWaFkyTnZkVzUwTG1OdmJTSUtmUT09 +kind: Secret +type: Opaque diff --git a/main.go b/main.go deleted file mode 100644 index 66e6965..0000000 --- a/main.go +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2017 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package main - -import ( - "flag" - "fmt" - "io/ioutil" - httplib "net/http" - "os" - "os/signal" - - "github.com/GoogleCloudPlatform/ubbagent/http" - "github.com/GoogleCloudPlatform/ubbagent/sdk" - "github.com/golang/glog" -) - -var configPath = flag.String("config", "", "configuration file") -var stateDir = flag.String("state-dir", "", "persistent state directory") -var noState = flag.Bool("no-state", false, "do not store persistent state") -var localPort = flag.Int("local-port", 0, "local HTTP daemon port") -var noHttp = flag.Bool("no-http", false, "do not start the HTTP daemon") - -// main is the entry point to the standalone agent. It constructs a new app.App with the config file -// specified using the --config flag, and it starts the http interface. SIGINT will initiate a -// graceful shutdown. -func main() { - flag.Parse() - - if *configPath == "" { - fmt.Fprintln(os.Stderr, "configuration file must be specified") - flag.Usage() - os.Exit(2) - } - - if *stateDir == "" && !*noState { - fmt.Fprintln(os.Stderr, "state directory must be specified (or use --no-state)") - flag.Usage() - os.Exit(2) - } - - if *localPort <= 0 && !*noHttp { - fmt.Fprintln(os.Stderr, "local-port must be > 0 (or use --no-http)") - flag.Usage() - os.Exit(2) - } - - configData, err := ioutil.ReadFile(*configPath) - if err != nil { - exitf("startup: failed to read configuration file: %+v", err) - } - - agent, err := sdk.NewAgent(configData, *stateDir) - if err != nil { - exitf("startup: failed to create agent: %+v", err) - } - - var rest *http.HttpInterface - if *localPort > 0 { - rest = http.NewHttpInterface(agent, *localPort) - if err := rest.Start(func(err error) { - // Process async http errors (which may be an immediate port in use error). - if err != httplib.ErrServerClosed { - exitf("http: %+v", err) - } - }); err != nil { - exitf("startup: %+v", err) - } - infof("Listening locally on port %v", *localPort) - } else { - infof("Not starting HTTP daemon") - } - - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt) - <-c - - infof("Shutting down...") - if rest != nil { - rest.Shutdown() - } - if err := agent.Shutdown(); err != nil { - glog.Warningf("shutdown: %+v", err) - } - glog.Flush() -} - -// infof prints a message to stdout and also logs it to the INFO log. -func infof(format string, args ...interface{}) { - msg := fmt.Sprintf(format, args...) - fmt.Println(msg) - glog.Info(msg) -} - -// exitf prints a message to stderr, logs it to the FATAL log, and exits. -func exitf(format string, args ...interface{}) { - msg := fmt.Sprintf(format, args...) - fmt.Fprintln(os.Stderr, msg) - glog.Exit(msg) -}