Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@
--repo_url=https://github.com/GoogleCloudPlatform/cloud-run-hello.git"
```

## Test Instrumentless

Test getting a coupon from the instrumentless API:
```
go run ./cmd/instrumentless_test YOUR_EVENT $(gcloud auth print-access-token)
```

## Contributor License Agreement

Contributions to this project must be accompanied by a Contributor License
Expand Down
21 changes: 21 additions & 0 deletions cmd/cloudshell_open/billing.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,24 @@ func checkBillingEnabled(projectID string) (bool, error) {
}
return bo.BillingEnabled, nil
}

func billingAccounts() ([]cloudbilling.BillingAccount, error) {
var out []cloudbilling.BillingAccount

client, err := cloudbilling.NewService(context.TODO())
if err != nil {
return nil, fmt.Errorf("failed to initialize cloud billing client: %w", err)
}
billingAccounts, err := client.BillingAccounts.List().Context(context.TODO()).Do()
if err != nil {
return nil, fmt.Errorf("failed to query billing accounts: %w", err)
}

for _, p := range billingAccounts.BillingAccounts {
if p.Open {
out = append(out, *p)
}
}

return out, nil
}
109 changes: 100 additions & 9 deletions cmd/cloudshell_open/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import (
"errors"
"flag"
"fmt"
"github.com/AlecAivazis/survey/v2"
"github.com/GoogleCloudPlatform/cloud-run-button/cmd/instrumentless"
"google.golang.org/api/transport"
"os"
"path/filepath"
"strings"
Expand All @@ -41,7 +44,8 @@ const (
reauthCredentialsWaitTimeout = time.Minute * 2
reauthCredentialsPollingInterval = time.Second

projectCreateURL = "https://console.cloud.google.com/cloud-resource-manager"
billingCreateURL = "https://console.cloud.google.com/billing/create"
instrumentlessEvent = "crbutton"
Comment thread
jamesward marked this conversation as resolved.
)

var (
Expand Down Expand Up @@ -205,11 +209,16 @@ func run(opts runOpts) error {
}

if len(projects) == 0 {
coupon, err := instrumentlessCoupon()
if err != nil {
return fmt.Errorf("could not get instrumentless coupon: %v", err)
}

fmt.Print(errorPrefix+" "+
warningLabel.Sprint("You don't have any GCP projects to deploy into!")+
"\n 1. Visit "+linkLabel.Sprint(projectCreateURL),
"\n 2. Create a new GCP project with a billing account",
"\n 3. Once you're done, press "+parameterLabel.Sprint("Enter")+" to continue: ")
"\n 1. Setup project using a starter coupon:"+
"\n "+linkLabel.Sprint(coupon.URL),
"\n 2. Once you're done, press "+parameterLabel.Sprint("Enter")+" to continue: ")
if _, err := bufio.NewReader(os.Stdin).ReadBytes('\n'); err != nil {
return err
}
Expand All @@ -228,14 +237,48 @@ func run(opts runOpts) error {
}

if err := waitForBilling(project, func(p string) error {
fmt.Print(errorPrefix+" "+
warningLabel.Sprint("GCP project you chose does not have an active billing account!")+
"\n 1. Visit "+linkLabel.Sprint(projectCreateURL),
"\n 2. Associate a billing account for project "+parameterLabel.Sprint(p),
"\n 3. Once you're done, press "+parameterLabel.Sprint("Enter")+" to continue: ")
projectLabel := color.New(color.Bold, color.FgHiCyan).Sprint(project)

fmt.Println(fmt.Sprintf(errorPrefix+" Project %s does not have an active billing account!", projectLabel))

billingAccounts, err := billingAccounts()
if err != nil {
return fmt.Errorf("could not get billing accounts: %v", err)
}

useExisting := false

if len(billingAccounts) > 0 {
useExisting, err = prompUseExistingBillingAccount(project)
if err != nil {
return err
}
}

if !useExisting {
err := promptInstrumentless()
if err != nil {
fmt.Println(infoPrefix + " Create a new billing account:")
fmt.Println(" " + linkLabel.Sprint(billingCreateURL))
fmt.Println(questionPrefix + " " + "Once you're done, press " + parameterLabel.Sprint("Enter") + " to continue: ")

if _, err := bufio.NewReader(os.Stdin).ReadBytes('\n'); err != nil {
return err
}
}
}

fmt.Println(infoPrefix + " Link the billing account to the project:" +
"\n " + linkLabel.Sprintf("https://console.cloud.google.com/billing?project=%s", project))

fmt.Println(questionPrefix + " " + "Once you're done, press " + parameterLabel.Sprint("Enter") + " to continue: ")

if _, err := bufio.NewReader(os.Stdin).ReadBytes('\n'); err != nil {
return err
}

// TODO(jamesward) automatically set billing account on project

return nil
}); err != nil {
return err
Expand Down Expand Up @@ -549,3 +592,51 @@ func isSubPath(a, b string) (bool, error) {
}
return !strings.HasPrefix(v, ".."+string(os.PathSeparator)), nil
}

func instrumentlessCoupon() (*instrumentless.Coupon, error) {
ctx := context.TODO()

creds, err := transport.Creds(ctx)
if err != nil {
return nil, fmt.Errorf("could not get user credentials: %v", err)
}

token, err := creds.TokenSource.Token()
if err != nil {
return nil, fmt.Errorf("could not get an auth token: %v", err)
}

return instrumentless.GetCoupon(instrumentlessEvent, token.AccessToken)
}

func promptInstrumentless() error {
coupon, err := instrumentlessCoupon()
if err != nil {
return fmt.Errorf("could not get instrumentless coupon: %v", err)
}

fmt.Println(infoPrefix + " Apply a starter coupon to create a billing account:" +
"\n " + linkLabel.Sprint(coupon.URL))

fmt.Println(questionPrefix + " Once you're done, press " + parameterLabel.Sprint("Enter") + " to continue: ")

if _, err := bufio.NewReader(os.Stdin).ReadBytes('\n'); err != nil {
return err
}

return nil
}

func prompUseExistingBillingAccount(project string) (bool, error) {
useExisting := false

projectLabel := color.New(color.Bold, color.FgHiCyan).Sprint(project)

if err := survey.AskOne(&survey.Confirm{
Default: false,
Message: fmt.Sprintf("Would you like to use an existing billing account with project %s?", projectLabel),
}, &useExisting, surveyIconOpts); err != nil {
return false, fmt.Errorf("could not prompt for confirmation %+v", err)
}
return useExisting, nil
}
4 changes: 3 additions & 1 deletion cmd/cloudshell_open/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ func listProjects() ([]string, error) {
var out []string
if err := client.Projects.List().PageSize(1000).Pages(context.TODO(), func(resp *cloudresourcemanager.ListProjectsResponse) error {
for _, p := range resp.Projects {
out = append(out, p.ProjectId)
if p.LifecycleState == "ACTIVE" {
out = append(out, p.ProjectId)
}
}
return nil
}); err != nil {
Expand Down
58 changes: 58 additions & 0 deletions cmd/instrumentless/instrumentless.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2019 Google LLC
//
// 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 instrumentless

import (
"encoding/json"
"errors"
"fmt"
"net/http"
)

type Coupon struct {
URL string `json:"url"`
}

func GetCoupon(event string, bearerToken string) (*Coupon, error) {
url := fmt.Sprintf("https://api.gcpcredits.com/%s", event)

req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}

req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", bearerToken))

res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}

if res.StatusCode != 200 {
return nil, errors.New(res.Status)
}

if res.Body != nil {
defer res.Body.Close()
}

coupon := Coupon{}
err = json.NewDecoder(res.Body).Decode(&coupon)
if err != nil {
return nil, fmt.Errorf("failed to read and parse the response: %v", err)
}

return &coupon, nil
}
42 changes: 42 additions & 0 deletions cmd/instrumentless_test/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright 2019 Google LLC
Comment thread
ahmetb marked this conversation as resolved.
//
// 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 (
"fmt"
"os"

"github.com/GoogleCloudPlatform/cloud-run-button/cmd/instrumentless"
)

func main() {

if len(os.Args) < 3 {
fmt.Fprintln(os.Stderr, "Not enough args")
os.Exit(1)
}

event := os.Args[1]
token := os.Args[2]

coupon, err := instrumentless.GetCoupon(event, token)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}

fmt.Printf("Got coupon: %s\n", coupon.URL)

}