mirror of https://github.com/perkeep/perkeep.git
Merge "pkg/deploy/gce: also create Google Cloud Project for user"
This commit is contained in:
commit
8a545a1ed2
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 > APIs > Google Cloud Storage</li>
|
||||
<li>APIs and auth > APIs > Google Cloud Storage JSON API</li>
|
||||
<li>APIs and auth > APIs > Google Compute Engine</li>
|
||||
<li>APIs and auth > APIs > 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>
|
||||
|
|
9469
vendor/google.golang.org/api/cloudresourcemanager/v1/cloudresourcemanager-gen.go
generated
vendored
Normal file
9469
vendor/google.golang.org/api/cloudresourcemanager/v1/cloudresourcemanager-gen.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -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, " ")
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
8584
vendor/google.golang.org/api/servicemanagement/v1/servicemanagement-gen.go
generated
vendored
Normal file
8584
vendor/google.golang.org/api/servicemanagement/v1/servicemanagement-gen.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue