pulumi/cmd/api.go
Chris Smith d5846d7e16 Add login and logout commands. (#437)
This PR adds `login` and `logout` commands to the `pulumi` CLI.

Rather than requiring a user name and password like before, we instead require users to login with GitHub credentials on the Pulumi Console website. (You can do this now via https://beta.moolumi.io.) Once there, the account page will show you an "access token" you can use to authenticate against the CLI.

Upon successful login, the user's credentials will be stored in `~/.pulumi/credentials.json`. This credentials file will be automatically read with the credentials added to every call to `PulumiRESTCall`.
2017-10-19 15:22:07 -07:00

94 lines
2.8 KiB
Go

package cmd
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"github.com/pulumi/pulumi/pkg/apitype"
"github.com/pulumi/pulumi/pkg/util/contract"
)
// pulumiCloudEndpoint returns the endpoint for the Pulumi Cloud Management Console API.
// e.g. "http://localhost:8080" or "https://api.moolumi.io".
func pulumiConsoleAPI() (string, error) {
envVar := os.Getenv("PULUMI_API")
if envVar == "" {
return "", fmt.Errorf("PULUMI_API env var not set")
}
return envVar, nil
}
// pulumiAPICall makes an HTTP request to the Pulumi API.
func pulumiAPICall(method string, path string, body []byte, accessToken string) (*http.Response, error) {
apiEndpoint, err := pulumiConsoleAPI()
if err != nil {
return nil, fmt.Errorf("getting Pulumi API endpoint: %v", err)
}
url := fmt.Sprintf("%s/api%s", apiEndpoint, path)
req, err := http.NewRequest(method, url, bytes.NewBuffer(body))
if err != nil {
return nil, fmt.Errorf("creating new HTTP request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
// Apply credentials if provided.
if accessToken != "" {
req.Header.Set("Authorization", fmt.Sprintf("token %s", accessToken))
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("performing HTTP request: %v", err)
}
return resp, nil
}
// pulumiRESTCallWithAccessToken calls the pulumi REST API marshalling reqObj to JSON and using
// that as the request body (use nil for GETs), and if successful, marshalling the responseObj
// as JSON and storing it in respObj (use nil for NoContent). The error return type might be an
// instance of apitype.ErrorResponse, in which case will have the response code.
func pulumiRESTCallWithAccessToken(method, path string, reqObj interface{}, respObj interface{}, token string) error {
var reqBody []byte
var err error
if reqObj != nil {
reqBody, err = json.Marshal(reqObj)
if err != nil {
return fmt.Errorf("marshalling request object as JSON: %v", err)
}
}
resp, err := pulumiAPICall(method, path, reqBody, token)
if err != nil {
return fmt.Errorf("calling API: %v", err)
}
defer contract.IgnoreClose(resp.Body)
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("reading response from API: %v", err)
}
// 4xx and 5xx responses should be of type ErrorResponse. See if we can unmarshal as that
// type, and if not just return the raw response text.
if resp.StatusCode >= 400 && resp.StatusCode <= 599 {
var errResp apitype.ErrorResponse
if err = json.Unmarshal(respBody, &errResp); err != nil {
errResp.Code = resp.StatusCode
errResp.Message = string(respBody)
}
return &errResp
}
if respObj != nil {
if err = json.Unmarshal(respBody, respObj); err != nil {
return fmt.Errorf("unmarshalling response object: %v", err)
}
}
return nil
}