Skip to content
This repository has been archived by the owner on Apr 20, 2021. It is now read-only.

Commit

Permalink
Merge pull request weaveworks#41 from weaveworks/user-tokens
Browse files Browse the repository at this point in the history
Add experimental support for user tokens
  • Loading branch information
tomwilkie authored Nov 4, 2016
2 parents 245ed26 + 354e083 commit d00033f
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 18 deletions.
66 changes: 55 additions & 11 deletions cmd/wcloud/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,24 @@ func env(key, def string) string {
return def
}

var (
token = env("SERVICE_TOKEN", "")
baseURL = env("BASE_URL", "https://cloud.weave.works")
)
const cliConfigFile = "~/.wcloudconfig"

func usage() {
fmt.Println(`Usage:
fmt.Printf(`Usage: wcloud COMMAND ...
deploy <image>:<version> Deploy image to your configured env
list List recent deployments
config (<filename>) Get (or set) the configured env
logs <deploy> Show lots for the given deployment`)
logs <deploy> Show lots for the given deployment
Environment Variables:
SERVICE_TOKEN Set the service token to use, overrides %s
BASE_URL Set the deploy to connect to, overrides %s
INSTANCE Set the remote instance id, overrides %s
`,
cliConfigFile,
cliConfigFile,
cliConfigFile,
)
}

func main() {
Expand All @@ -55,7 +62,23 @@ func main() {
os.Exit(1)
}

c := NewClient(token, baseURL)
cliConfig, err := loadCLIConfig()
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
token := env("SERVICE_TOKEN", cliConfig.ServiceToken)
baseURL := env("BASE_URL", cliConfig.BaseURL)
instance := env("INSTANCE", cliConfig.Instance)
if baseURL == "" {
baseURL = "https://cloud.weave.works"
}

c, err := NewClient(token, baseURL, instance)
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}

switch os.Args[1] {
case "deploy":
Expand All @@ -75,9 +98,15 @@ func main() {
}
}

func newFlagSet() *flag.FlagSet {
flags := flag.NewFlagSet("", flag.ContinueOnError)
flags.Usage = usage
return flags
}

func deploy(c Client, args []string) {
var (
flags = flag.NewFlagSet("", flag.ContinueOnError)
flags = newFlagSet()
username = flags.String("u", "", "Username to report to deploy service (default with be current user)")
services ArrayFlags
)
Expand Down Expand Up @@ -118,7 +147,7 @@ func deploy(c Client, args []string) {

func list(c Client, args []string) {
var (
flags = flag.NewFlagSet("", flag.ContinueOnError)
flags = newFlagSet()
since = flags.Duration("since", 7*24*time.Hour, "How far back to fetch results")
)
if err := flags.Parse(args); err != nil {
Expand Down Expand Up @@ -151,7 +180,7 @@ func list(c Client, args []string) {

func events(c Client, args []string) {
var (
flags = flag.NewFlagSet("", flag.ContinueOnError)
flags = newFlagSet()
since = flags.Duration("since", 7*24*time.Hour, "How far back to fetch results")
)
if err := flags.Parse(args); err != nil {
Expand Down Expand Up @@ -185,7 +214,7 @@ func loadConfig(filename string) (*Config, error) {
return nil, err
}
}
return &config, nil
return &config, err
}

func config(c Client, args []string) {
Expand Down Expand Up @@ -222,6 +251,21 @@ func config(c Client, args []string) {
}
}

func loadCLIConfig() (CLIConfig, error) {
buf, err := ioutil.ReadFile(cliConfigFile)
if err != nil {
if os.IsNotExist(err) {
return CLIConfig{}, nil
}
return CLIConfig{}, err
}
var cliConfig CLIConfig
if err := yaml.Unmarshal(buf, &cliConfig); err != nil {
return CLIConfig{}, err
}
return cliConfig, err
}

func logs(c Client, args []string) {
if len(args) != 1 {
usage()
Expand Down
66 changes: 59 additions & 7 deletions cmd/wcloud/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,76 @@ import (

// Client for the deployment service
type Client struct {
token string
baseURL string
token string
baseURL string
instance string // TODO: Use this in urls
authType string
}

// NewClient makes a new Client
func NewClient(token, baseURL string) Client {
return Client{
token: token,
baseURL: baseURL,
func NewClient(token, baseURL, instance string) (Client, error) {
c := Client{
token: token,
baseURL: baseURL,
instance: instance,
authType: "Scope-User",
}

// TODO: Detect the type of token and get the instance id separately
if instance == "" {
err := c.getInstanceID()
if err == ErrUnauthorized {
c.authType = "Scope-Probe"
err = c.getInstanceID()
}
if err != nil {
return Client{}, err
}
}

if c.authType == "Scope-User" {
c.baseURL = fmt.Sprintf("%s/api/app/%s", c.baseURL, c.instance)
}

return c, nil
}

func (c *Client) getInstanceID() error {
// User did not provide an instance, check if we can auto-detect only 1 instance
req, err := http.NewRequest("GET", c.baseURL+"/api/users/lookup", nil)
if err != nil {
return err
}
req.Header.Add("Authorization", fmt.Sprintf("%s token=%s", c.authType, c.token))
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
if res.StatusCode == http.StatusUnauthorized {
return ErrUnauthorized
}
if res.StatusCode != http.StatusOK {
return fmt.Errorf("Error initializing client: %d\n", res.StatusCode)
}

defer res.Body.Close()
var lookup lookupView
if err := json.NewDecoder(res.Body).Decode(&lookup); err != nil {
return err
}
if len(lookup.Instances) != 1 {
return ErrMultipleInstances(lookup)
}
c.instance = lookup.Instances[0].ExternalID
return nil
}

func (c Client) newRequest(method, path string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequest(method, c.baseURL+path, body)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", fmt.Sprintf("Scope-Probe token=%s", c.token))
req.Header.Add("Authorization", fmt.Sprintf("%s token=%s", c.authType, c.token))
return req, nil
}

Expand Down
25 changes: 25 additions & 0 deletions cmd/wcloud/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package main

import (
"errors"
"fmt"
"strings"
)

// ErrUnauthorized is the error when a user is unauthorized
var ErrUnauthorized = errors.New("unauthorized")

// ErrMultipleInstances is the error when a user has access to multiple
// instances, but we don't know which one to use.
type ErrMultipleInstances lookupView

func (e ErrMultipleInstances) Error() string {
if len(e.Instances) == 0 {
return "no available instances"
}
var instances []string
for _, i := range e.Instances {
instances = append(instances, fmt.Sprintf("%s (%s)", i.Name, i.ExternalID))
}
return fmt.Sprintf("multiple available instances: %s", strings.Join(instances, ", "))
}
18 changes: 18 additions & 0 deletions cmd/wcloud/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,21 @@ type NotificationConfig struct {
MessageTemplate string `json:"message_template" yaml:"message_template"`
ApplyMessageTemplate string `json:"apply_message_template" yaml:"apply_message_template"`
}

// CLIConfig is used to store local wcloud cli configs
type CLIConfig struct {
ServiceToken string `yaml:"service_token"`
BaseURL string `yaml:"base_url"`
Instance string `yaml:"instance,omitempty"`
}

// lookupView is returned from /api/users/lookup. Only includes the fields we care about.
type lookupView struct {
Instances []Instance `json:"organizations,omitempty"`
}

// Instance is a helper for data returned as part of the lookupView.
type Instance struct {
ExternalID string `json:"id"`
Name string `json:"name"`
}

0 comments on commit d00033f

Please sign in to comment.