Merge "pkg/deploy/gce: also create Google Cloud Project for user"

This commit is contained in:
Mathieu Lonjaret 2017-04-21 16:16:34 +00:00 committed by Gerrit Code Review
commit 8a545a1ed2
9 changed files with 18545 additions and 33 deletions

View File

@ -23,6 +23,7 @@ import (
"errors"
"fmt"
"log"
"math/big"
"net/http"
"path"
"path/filepath"
@ -39,8 +40,10 @@ import (
"golang.org/x/net/context"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
cloudresourcemanager "google.golang.org/api/cloudresourcemanager/v1"
compute "google.golang.org/api/compute/v1"
"google.golang.org/api/googleapi"
servicemanagement "google.golang.org/api/servicemanagement/v1"
storage "google.golang.org/api/storage/v1"
)
@ -83,6 +86,8 @@ func NewOAuthConfig(clientID, clientSecret string) *oauth2.Config {
logging.WriteScope,
compute.DevstorageFullControlScope,
compute.ComputeScope,
cloudresourcemanager.CloudPlatformScope,
servicemanagement.CloudPlatformScope,
"https://www.googleapis.com/auth/sqlservice",
"https://www.googleapis.com/auth/sqlservice.admin",
},
@ -94,11 +99,12 @@ func NewOAuthConfig(clientID, clientSecret string) *oauth2.Config {
// InstanceConf is the configuration for the Google Compute Engine instance that will be deployed.
type InstanceConf struct {
Name string // Name given to the virtual machine instance.
Project string // Google project ID where the instance is created.
Machine string // Machine type.
Zone string // GCE zone; see https://cloud.google.com/compute/docs/zones
Hostname string // Fully qualified domain name.
Name string // Name given to the virtual machine instance.
Project string // Google project ID where the instance is created.
CreateProject bool // CreateProject defines whether to first create project.
Machine string // Machine type.
Zone string // GCE zone; see https://cloud.google.com/compute/docs/zones
Hostname string // Fully qualified domain name.
configDir string // bucketBase() + "/config"
blobDir string // bucketBase() + "/blobs"
@ -224,6 +230,197 @@ func (e projectIDError) Error() string {
return fmt.Sprintf("project ID error for %v", e.id)
}
// CreateProject creates a new Google Cloud Project. It returns the project ID,
// which is a random number in (0,1e10), prefixed with "camlistore-launcher-".
func (d *Deployer) CreateProject(ctx context.Context) (string, error) {
s, err := cloudresourcemanager.New(d.Client)
if err != nil {
return "", err
}
// Allow for a few retries, when we generated an already taken project ID
creationTimeout := time.Now().Add(time.Minute)
var projectID, projectName string
for {
if d.Conf.Project != "" {
projectID = d.Conf.Project
projectName = projectID
} else {
projectID = genRandomProjectID()
projectName = strings.Replace(projectID, "camlistore-launcher", "Camlistore ", 1)
}
project := cloudresourcemanager.Project{
Name: projectName,
ProjectId: projectID,
}
if time.Now().After(creationTimeout) {
return "", errors.New("timeout while trying to create project")
}
d.Printf("Trying to create project %v", projectID)
op, err := cloudresourcemanager.NewProjectsService(s).Create(&project).Do()
if err != nil {
gerr, ok := err.(*googleapi.Error)
if !ok {
return "", fmt.Errorf("could not create project: %v", err)
}
if gerr.Code != 409 {
return "", fmt.Errorf("could not create project: %v", gerr.Message)
}
// it's ok using time.Sleep, and no backoff, as the
// timeout is pretty short, and we only retry on a 409.
time.Sleep(time.Second)
// retry if project ID already exists.
d.Printf("Project %v already exists, will retry with a new project ID", project.ProjectId)
continue
}
// as per
// https://cloud.google.com/resource-manager/reference/rest/v1/projects/create
// recommendation
timeout := time.Now().Add(30 * time.Second)
backoff := time.Second
startPolling := 5 * time.Second
time.Sleep(startPolling)
for {
if time.Now().After(timeout) {
return "", fmt.Errorf("timeout while trying to check project creation")
}
if !op.Done {
// it's ok to just sleep, as our timeout is pretty short.
time.Sleep(backoff)
backoff *= 2
op, err = cloudresourcemanager.NewOperationsService(s).Get(op.Name).Do()
if err != nil {
return "", fmt.Errorf("could not check project creation status: %v", err)
}
continue
}
if op.Error != nil {
// TODO(mpl): ghetto logging for now. detect at least the quota errors.
var details string
for _, v := range op.Error.Details {
details += string(v)
}
return "", fmt.Errorf("could not create project: %v, %v", op.Error.Message, details)
}
break
}
break
}
d.Printf("Success creating project %v", projectID)
return projectID, nil
}
func genRandomProjectID() string {
// we're allowed up to 30 characters, and we already consume 20 with
// "camlistore-launcher-", so we've got 10 chars left of randomness. Should
// be plenty enough I think.
var n *big.Int
var err error
zero := big.NewInt(0)
for {
n, err = rand.Int(rand.Reader, big.NewInt(1e10)) // max is 1e10 - 1
if err != nil {
panic(fmt.Sprintf("rand.Int error: %v", err))
}
if n.Cmp(zero) > 0 {
break
}
}
return fmt.Sprintf("camlistore-launcher-%d", n)
}
func (d *Deployer) enableAPIs() error {
// TODO(mpl): For now we're lucky enough that servicemanagement seems to
// work even when the Service Management API hasn't been enabled for the
// project. If/when it does not anymore, then we should use serviceuser
// instead. http://stackoverflow.com/a/43503392/1775619
s, err := servicemanagement.New(d.Client)
if err != nil {
return err
}
list, err := servicemanagement.NewServicesService(s).List().ConsumerId("project:" + d.Conf.Project).Do()
if err != nil {
return err
}
requiredServices := map[string]string{
"storage-component.googleapis.com": "Google Cloud Storage",
"storage-api.googleapis.com": "Google Cloud Storage JSON",
"logging.googleapis.com": "Stackdriver Logging",
"compute-component.googleapis.com": "Google Compute Engine",
}
enabledServices := make(map[string]bool)
for _, v := range list.Services {
enabledServices[v.ServiceName] = true
}
errc := make(chan error, len(requiredServices))
var wg sync.WaitGroup
for k, v := range requiredServices {
if _, ok := enabledServices[k]; ok {
continue
}
d.Printf("%v API not enabled; enabling it with Service Management", v)
op, err := servicemanagement.NewServicesService(s).
Enable(k, &servicemanagement.EnableServiceRequest{ConsumerId: "project:" + d.Conf.Project}).Do()
if err != nil {
gerr, ok := err.(*googleapi.Error)
if !ok {
return err
}
if gerr.Code != 400 {
return err
}
for _, v := range gerr.Errors {
if v.Reason == "failedPrecondition" && strings.Contains(v.Message, "billing-enabled") {
return fmt.Errorf("you need to enabling billing for project %v: https://console.cloud.google.com/billing/?project=%v", d.Conf.Project, d.Conf.Project)
}
}
return err
}
wg.Add(1)
go func(service, opName string) {
defer wg.Done()
timeout := time.Now().Add(2 * time.Minute)
backoff := time.Second
startPolling := 5 * time.Second
time.Sleep(startPolling)
for {
if time.Now().After(timeout) {
errc <- fmt.Errorf("timeout while trying to enable service: %v", service)
return
}
op, err := servicemanagement.NewOperationsService(s).Get(opName).Do()
if err != nil {
errc <- fmt.Errorf("could not check service enabling status: %v", err)
return
}
if !op.Done {
// it's ok to just sleep, as our timeout is pretty short.
time.Sleep(backoff)
backoff *= 2
continue
}
if op.Error != nil {
errc <- fmt.Errorf("could not enable service %v: %v", service, op.Error.Message)
return
}
d.Printf("%v service successfully enabled", service)
return
}
}(k, op.Name)
}
wg.Wait()
close(errc)
for err := range errc {
if err != nil {
return err
}
}
return nil
}
func (d *Deployer) checkProjectID() error {
// TODO(mpl): cache the computeService in Deployer, instead of recreating a new one everytime?
s, err := compute.New(d.Client)
@ -243,7 +440,7 @@ func (d *Deployer) checkProjectID() error {
if project.Name != d.Conf.Project {
return projectIDError{
id: d.Conf.Project,
cause: fmt.Errorf("project ID do not match: got %q, wanted %q", project.Name, d.Conf.Project),
cause: fmt.Errorf("project IDs do not match: got %q, wanted %q", project.Name, d.Conf.Project),
}
}
return nil
@ -274,6 +471,12 @@ func (d *Deployer) getInstanceAttribute(attr string) (string, error) {
// Create sets up and starts a Google Compute Engine instance as defined in d.Conf. It
// creates the necessary Google Storage buckets beforehand.
func (d *Deployer) Create(ctx context.Context) (*compute.Instance, error) {
if err := d.enableAPIs(); err != nil {
return nil, projectIDError{
id: d.Conf.Project,
cause: err,
}
}
if err := d.checkProjectID(); err != nil {
return nil, err
}

View File

@ -32,6 +32,7 @@ import (
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"time"
@ -341,6 +342,9 @@ func (h *DeployHandler) zoneValues() []string {
return h.regions
}
// if there's project as a query parameter, it means we've just created a
// project for them and we're redirecting them to the form, but with the projectID
// field pre-filled for them this time.
func (h *DeployHandler) serveRoot(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
h.serveSetup(w, r)
@ -354,9 +358,11 @@ func (h *DeployHandler) serveRoot(w http.ResponseWriter, r *http.Request) {
if r.FormValue("WIP") == "1" {
camliRev = "WORKINPROGRESS"
}
h.tplMu.RLock()
defer h.tplMu.RUnlock()
if err := h.tpl.ExecuteTemplate(w, "withform", &TemplateData{
ProjectID: r.FormValue("project"),
Prefix: h.prefix,
Help: h.help,
ZoneValues: h.zoneValues(),
@ -444,6 +450,23 @@ func (h *DeployHandler) serveCallback(w http.ResponseWriter, r *http.Request) {
Logger: h.logger,
}
// They've requested that we create a project for them.
if instConf.CreateProject {
// So we try to do so.
projectID, err := depl.CreateProject(context.TODO())
if err != nil {
// TODO(mpl): we log the errors, but none of them are
// visible to the user (they just get a 500). I should
// probably at least detect and report them the project
// creation quota errors.
h.serveError(w, r, err)
return
}
// And serve the form again if we succeeded.
http.Redirect(w, r, fmt.Sprintf("%s/?project=%s", h.prefix, projectID), http.StatusFound)
return
}
if found := h.serveOldInstance(w, br, depl); found {
return
}
@ -484,6 +507,9 @@ func (h *DeployHandler) serveCallback(w http.ResponseWriter, r *http.Request) {
h.recordStateErr[br.String()] = err
return
}
if state.Err != "" {
return
}
if instConf.Hostname != "" {
return
}
@ -686,9 +712,18 @@ func formValueOrDefault(r *http.Request, formField, defValue string) string {
}
func (h *DeployHandler) confFromForm(r *http.Request) (*InstanceConf, error) {
project := r.FormValue("project")
if project == "" {
return nil, errors.New("missing project parameter")
newProject, err := strconv.ParseBool(r.FormValue("newproject"))
if err != nil {
return nil, fmt.Errorf("could not convert \"newproject\" value to bool: %v", err)
}
var projectID string
if newProject {
projectID = r.FormValue("newprojectid")
} else {
projectID = r.FormValue("projectid")
if projectID == "" {
return nil, errors.New("missing project ID parameter")
}
}
var zone string
zoneReg := formValueOrDefault(r, "zone", DefaultRegion)
@ -701,13 +736,14 @@ func (h *DeployHandler) confFromForm(r *http.Request) (*InstanceConf, error) {
return nil, errors.New("invalid zone or region")
}
return &InstanceConf{
Name: formValueOrDefault(r, "name", DefaultInstanceName),
Project: project,
Machine: formValueOrDefault(r, "machine", DefaultMachineType),
Zone: zone,
Hostname: formValueOrDefault(r, "hostname", ""),
Ctime: time.Now(),
WIP: r.FormValue("WIP") == "1",
CreateProject: newProject,
Name: formValueOrDefault(r, "name", DefaultInstanceName),
Project: projectID,
Machine: formValueOrDefault(r, "machine", DefaultMachineType),
Zone: zone,
Hostname: formValueOrDefault(r, "hostname", ""),
Ctime: time.Now(),
WIP: r.FormValue("WIP") == "1",
}, nil
}
@ -888,6 +924,7 @@ type TemplateData struct {
InstanceIP string `json:",omitempty"` // instance IP address that we display after successful creation.
InstanceHostname string `json:",omitempty"`
ProjectConsoleURL string
ProjectID string // set by us when we've just created a project on the behalf of the user
ZoneValues []string
MachineValues []string
CamliVersion string // git revision found in https://storage.googleapis.com/camlistore-release/docker/VERSION
@ -1043,6 +1080,50 @@ or corrupted.</p>
<html>
{{template "header" .}}
<body>
<!-- TODO(mpl): bundle jquery -->
<script type="text/javascript" src="https://code.jquery.com/jquery-3.2.1.min.js" ></script>
<!-- Change the text of the submit button, the billing URL, and the "disabled" of the input fiels, depending on whether we create a new project or use a selected one. -->
<script type="text/javascript">
$(document).ready(function(){
var setBillingURL = function() {
var projectID = $('#project_id').val();
if (projectID != "") {
$('#billing_url').attr("href", "https://console.cloud.google.com/billing/?project="+projectID);
} else {
$('#billing_url').attr("href", "https://console.cloud.google.com/billing/");
}
};
setBillingURL();
var toggleFormAction = function(newProject) {
if (newProject == "true") {
$('#zone').prop("disabled", true);
$('#machine').prop("disabled", true);
$('#submit_btn').val("Create project");
return
}
$('#zone').prop("disabled", false);
$('#machine').prop("disabled", false);
$('#submit_btn').val("Create instance");
};
$("#new_project_id").focus(function(){
$('#newproject_yes').prop("checked", true);
toggleFormAction("true");
});
$("#project_id").focus(function(){
$('#newproject_no').prop("checked", true);
toggleFormAction("false");
});
$("#project_id").bind('input', function(e){
setBillingURL();
});
$("#newproject_yes").change(function(e){
toggleFormAction("true");
});
$("#newproject_no").change(function(e){
toggleFormAction("false");
});
})
</script>
{{if .InstanceKey}}
<div style="z-index:0; -webkit-filter: blur(5px);">
{{end}}
@ -1073,36 +1154,49 @@ and visit both the "Compute Engine" and "Storage" sections for your project.
{{end}}
<table border=0 cellpadding=3 style='margin-top: 2em'>
<tr valign=top><td align=right><nobr>Google Project ID:</nobr></td><td margin=left><input name="project" size=30 value=""><br>
<ul style="padding-left:0;margin-left:0;font-size:75%">
<li>Select a <a href="https://console.developers.google.com/project">Google Project</a> in which to create the VM. If it doesn't already exist, <a href="https://console.developers.google.com/project">create it</a> first before using this Camlistore creation tool.</li>
<li>Requirements:</li>
<ul>
<li>Enable billing. (Billing & settings)</li>
<li>APIs and auth &gt APIs &gt Google Cloud Storage</li>
<li>APIs and auth &gt APIs &gt Google Cloud Storage JSON API</li>
<li>APIs and auth &gt APIs &gt Google Compute Engine</li>
<li>APIs and auth &gt APIs &gt Google Cloud Logging API</li>
</ul>
</ul>
<tr valign=top><td align=right><nobr>Google Project ID:</nobr></td><td margin=left style='width:1%;white-space:nowrap;'>
{{if .ProjectID}}
<input id='newproject_yes' type="radio" name="newproject" value="true"> <label for="newproject_yes">Create a new project: </label></td><td align=left><input id='new_project_id' name="newprojectid" size=30 placeholder="Leave blank for auto-generated"></td></tr><tr valign=top><td></td><td>
<input id='newproject_no' type="radio" name="newproject" value="false" checked='checked'> <a href="https://console.cloud.google.com/iam-admin/projects">Existing project</a> ID: </td><td align=left><input id='project_id' name="projectid" size=30 value="{{.ProjectID}}"></td></tr><tr valign=top><td></td><td colspan="2">
{{else}}
<input id='newproject_yes' type="radio" name="newproject" value="true" checked='checked'> <label for="newproject_yes">Create a new project: </label></td><td align=left><input id='new_project_id' name="newprojectid" size=30 placeholder="Leave blank for auto-generated"></td></tr><tr valign=top><td></td><td>
<input id='newproject_no' type="radio" name="newproject" value="false"> <a href="https://console.cloud.google.com/iam-admin/projects">Existing project</a> ID: </td><td align=left><input id='project_id' name="projectid" size=30 value="{{.ProjectID}}"></td></tr><tr valign=top><td></td><td colspan="2">
{{end}}
<span style="padding-left:0;margin-left:0">You need to <a id='billing_url' href="https://console.cloud.google.com/billing">enable billing</a> with Google for the selected project.</span>
</td></tr>
<tr valign=top><td align=right><nobr><a href="{{.Help.zones}}">Zone</a> or Region</nobr>:</td><td>
<input name="zone" list="regions" value="` + DefaultRegion + `">
{{if .ProjectID}}
<input id='zone' name="zone" list="regions" value="` + DefaultRegion + `">
{{else}}
<input id='zone' name="zone" list="regions" value="` + DefaultRegion + `" disabled='disabled'>
{{end}}
<datalist id="regions">
{{range $k, $v := .ZoneValues}}
<option value={{$v}}>{{$v}}</option>
{{end}}
</datalist><br/><span style="font-size:75%">If a region is specified, a random zone (-a, -b, -c, etc) in that region will be selected.</span>
</datalist></td></tr>
<tr valign=top><td></td><td colspan="2"><span style="font-size:75%">If a region is specified, a random zone (-a, -b, -c, etc) in that region will be selected.</span>
</td></tr>
<tr valign=top><td align=right><a href="{{.Help.machineTypes}}">Machine type</a>:</td><td>
<input name="machine" list="machines" value="g1-small">
{{if .ProjectID}}
<input id='machine' name="machine" list="machines" value="g1-small">
{{else}}
<input id='machine' name="machine" list="machines" value="g1-small" disabled='disabled'>
{{end}}
<datalist id="machines">
{{range $k, $v := .MachineValues}}
<option value={{$v}}>{{$v}}</option>
{{end}}
</datalist><br/><span style="font-size:75%">As of 2015-12-27, a g1-small is $13.88 (USD) per month, before storage usage charges. See <a href="https://cloud.google.com/compute/pricing#machinetype">current pricing</a>.</span>
</datalist></td></tr>
<tr valign=top><td></td><td colspan="2"><span style="font-size:75%">As of 2015-12-27, a g1-small is $13.88 (USD) per month, before storage usage charges. See <a href="https://cloud.google.com/compute/pricing#machinetype">current pricing</a>.</span>
</td></tr>
<tr><td></td><td>
{{if .ProjectID}}
<input id='submit_btn' type='submit' value="Create instance" style='background: #eee; padding: 0.8em; font-weight: bold'><br><span style="font-size:75%">(it will ask for permissions)</span>
{{else}}
<input id='submit_btn' type='submit' value="Create project" style='background: #eee; padding: 0.8em; font-weight: bold'><br><span style="font-size:75%">(it will ask for permissions)</span>
{{end}}
</td></tr>
<tr><td></td><td><input type='submit' value="Create instance" style='background: #ffdb00; padding: 0.5em; font-weight: bold'><br><span style="font-size:75%">(it will ask for permissions)</span></td></tr>
</table>
</form>
</div>

File diff suppressed because it is too large Load Diff

22
vendor/google.golang.org/api/gensupport/header.go generated vendored Normal file
View File

@ -0,0 +1,22 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gensupport
import (
"fmt"
"runtime"
"strings"
)
// GoogleClientHeader returns the value to use for the x-goog-api-client
// header, which is used internally by Google.
func GoogleClientHeader(generatorVersion, clientElement string) string {
elts := []string{"gl-go/" + strings.Replace(runtime.Version(), " ", "_", -1)}
if clientElement != "" {
elts = append(elts, clientElement)
}
elts = append(elts, fmt.Sprintf("gdcl/%s", generatorVersion))
return strings.Join(elts, " ")
}

28
vendor/google.golang.org/api/gensupport/header_test.go generated vendored Normal file
View File

@ -0,0 +1,28 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gensupport
import (
"fmt"
"runtime"
"strings"
"testing"
)
func TestGoogleClientHeader(t *testing.T) {
const genVersion = "20170101"
gv := strings.Replace(runtime.Version(), " ", "_", -1)
got := GoogleClientHeader(genVersion, "gccl/xyz")
want := fmt.Sprintf("gl-go/%s gccl/xyz gdcl/%s", gv, genVersion)
if got != want {
t.Errorf("got %q, want %q", got, want)
}
got = GoogleClientHeader(genVersion, "")
want = fmt.Sprintf("gl-go/%s gdcl/%s", gv, genVersion)
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}

57
vendor/google.golang.org/api/gensupport/jsonfloat.go generated vendored Normal file
View File

@ -0,0 +1,57 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// 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
//
// http://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 gensupport
import (
"encoding/json"
"errors"
"fmt"
"math"
)
// JSONFloat64 is a float64 that supports proper unmarshaling of special float
// values in JSON, according to
// https://developers.google.com/protocol-buffers/docs/proto3#json. Although
// that is a proto-to-JSON spec, it applies to all Google APIs.
//
// The jsonpb package
// (https://github.com/golang/protobuf/blob/master/jsonpb/jsonpb.go) has
// similar functionality, but only for direct translation from proto messages
// to JSON.
type JSONFloat64 float64
func (f *JSONFloat64) UnmarshalJSON(data []byte) error {
var ff float64
if err := json.Unmarshal(data, &ff); err == nil {
*f = JSONFloat64(ff)
return nil
}
var s string
if err := json.Unmarshal(data, &s); err == nil {
switch s {
case "NaN":
ff = math.NaN()
case "Infinity":
ff = math.Inf(1)
case "-Infinity":
ff = math.Inf(-1)
default:
return fmt.Errorf("google.golang.org/api/internal: bad float string %q", s)
}
*f = JSONFloat64(ff)
return nil
}
return errors.New("google.golang.org/api/internal: data not float or string")
}

View File

@ -0,0 +1,53 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// 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
//
// http://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 gensupport
import (
"encoding/json"
"math"
"testing"
)
func TestJSONFloat(t *testing.T) {
for _, test := range []struct {
in string
want float64
}{
{"0", 0},
{"-10", -10},
{"1e23", 1e23},
{`"Infinity"`, math.Inf(1)},
{`"-Infinity"`, math.Inf(-1)},
{`"NaN"`, math.NaN()},
} {
var f64 JSONFloat64
if err := json.Unmarshal([]byte(test.in), &f64); err != nil {
t.Fatal(err)
}
got := float64(f64)
if got != test.want && math.IsNaN(got) != math.IsNaN(test.want) {
t.Errorf("%s: got %f, want %f", test.in, got, test.want)
}
}
}
func TestJSONFloatErrors(t *testing.T) {
var f64 JSONFloat64
for _, in := range []string{"", "a", `"Inf"`, `"-Inf"`, `"nan"`, `"nana"`} {
if err := json.Unmarshal([]byte(in), &f64); err == nil {
t.Errorf("%q: got nil, want error", in)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -56,6 +56,7 @@ import (
"golang.org/x/crypto/acme/autocert"
"golang.org/x/net/context"
"golang.org/x/oauth2/google"
cloudresourcemanager "google.golang.org/api/cloudresourcemanager/v1"
compute "google.golang.org/api/compute/v1"
"google.golang.org/api/option"
storageapi "google.golang.org/api/storage/v1"
@ -580,6 +581,7 @@ var launchConfig = &cloudlaunch.Config{
compute.ComputeScope,
logging.WriteScope,
datastore.ScopeDatastore,
cloudresourcemanager.CloudPlatformScope,
},
}