github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/cmd/juju/romulus/agree/agree.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package agree
     5  
     6  import (
     7  	"bufio"
     8  	"bytes"
     9  	"fmt"
    10  	"os"
    11  	"os/exec"
    12  	"strings"
    13  
    14  	"github.com/juju/cmd"
    15  	"github.com/juju/errors"
    16  	"github.com/juju/gnuflag"
    17  	"github.com/juju/juju/cmd/modelcmd"
    18  	"github.com/juju/juju/cmd/output"
    19  	"github.com/juju/terms-client/api"
    20  	"github.com/juju/terms-client/api/wireformat"
    21  	"gopkg.in/juju/charm.v6-unstable"
    22  )
    23  
    24  var (
    25  	clientNew = api.NewClient
    26  )
    27  
    28  const agreeDoc = `
    29  Agree to the terms required by a charm.
    30  
    31  When deploying a charm that requires agreement to terms, use 'juju agree' to
    32  view the terms and agree to them. Then the charm may be deployed.
    33  
    34  Once you have agreed to terms, you will not be prompted to view them again.
    35  
    36  Examples:
    37      # Displays terms for somePlan revision 1 and prompts for agreement.
    38      juju agree somePlan/1
    39  
    40      # Displays the terms for revision 1 of somePlan, revision 2 of otherPlan,
    41      # and prompts for agreement.
    42      juju agree somePlan/1 otherPlan/2
    43  
    44      # Agrees to the terms without prompting.
    45      juju agree somePlan/1 otherPlan/2 --yes
    46  `
    47  
    48  // NewAgreeCommand returns a new command that can be
    49  // used to create user agreements.
    50  func NewAgreeCommand() cmd.Command {
    51  	return &agreeCommand{}
    52  }
    53  
    54  type term struct {
    55  	owner    string
    56  	name     string
    57  	revision int
    58  }
    59  
    60  // agreeCommand creates a user agreement to the specified terms.
    61  type agreeCommand struct {
    62  	modelcmd.JujuCommandBase
    63  	out cmd.Output
    64  
    65  	terms           []term
    66  	termIds         []string
    67  	SkipTermContent bool
    68  }
    69  
    70  // SetFlags implements Command.SetFlags.
    71  func (c *agreeCommand) SetFlags(f *gnuflag.FlagSet) {
    72  	c.JujuCommandBase.SetFlags(f)
    73  	f.BoolVar(&c.SkipTermContent, "yes", false, "Agree to terms non interactively")
    74  	c.out.AddFlags(f, "json", output.DefaultFormatters)
    75  }
    76  
    77  // Info implements Command.Info.
    78  func (c *agreeCommand) Info() *cmd.Info {
    79  	return &cmd.Info{
    80  		Name:    "agree",
    81  		Args:    "<term>",
    82  		Purpose: "Agree to terms.",
    83  		Doc:     agreeDoc,
    84  	}
    85  }
    86  
    87  // Init read and verifies the arguments.
    88  func (c *agreeCommand) Init(args []string) error {
    89  	if len(args) < 1 {
    90  		return errors.New("missing arguments")
    91  	}
    92  
    93  	for _, t := range args {
    94  		termId, err := charm.ParseTerm(t)
    95  		if err != nil {
    96  			return errors.Annotate(err, "invalid term format")
    97  		}
    98  		if termId.Revision == 0 {
    99  			return errors.Errorf("must specify a valid term revision %q", t)
   100  		}
   101  		c.terms = append(c.terms, term{owner: termId.Owner, name: termId.Name, revision: termId.Revision})
   102  		c.termIds = append(c.termIds, t)
   103  	}
   104  	if len(c.terms) == 0 {
   105  		return errors.New("must specify a valid term revision")
   106  	}
   107  	return nil
   108  }
   109  
   110  // Run implements Command.Run.
   111  func (c *agreeCommand) Run(ctx *cmd.Context) error {
   112  	client, err := c.BakeryClient()
   113  	if err != nil {
   114  		return errors.Trace(err)
   115  	}
   116  
   117  	termsClient, err := clientNew(api.HTTPClient(client))
   118  	if err != nil {
   119  		return err
   120  	}
   121  
   122  	if c.SkipTermContent {
   123  		err := saveAgreements(ctx, termsClient, c.terms)
   124  		if err != nil {
   125  			return errors.Trace(err)
   126  		}
   127  		return nil
   128  	}
   129  
   130  	needAgreement := []wireformat.GetTermsResponse{}
   131  	terms, err := termsClient.GetUnsignedTerms(&wireformat.CheckAgreementsRequest{
   132  		Terms: c.termIds,
   133  	})
   134  	if err != nil {
   135  		return errors.Annotate(err, "failed to retrieve terms")
   136  	}
   137  	needAgreement = append(needAgreement, terms...)
   138  
   139  	if len(needAgreement) == 0 {
   140  		fmt.Fprintf(ctx.Stdout, "Already agreed\n")
   141  		return nil
   142  	}
   143  
   144  	err = printTerms(ctx, needAgreement)
   145  	if err != nil {
   146  		return errors.Trace(err)
   147  	}
   148  	fmt.Fprintf(ctx.Stdout, "Do you agree to the displayed terms? (Y/n): ")
   149  	answer, err := userAnswer()
   150  	if err != nil {
   151  		return errors.Trace(err)
   152  	}
   153  
   154  	agreedTerms := make([]term, len(needAgreement))
   155  	for i, t := range needAgreement {
   156  		agreedTerms[i] = term{owner: t.Owner, name: t.Name, revision: t.Revision}
   157  	}
   158  
   159  	answer = strings.TrimSpace(answer)
   160  	if userAgrees(answer) {
   161  		err = saveAgreements(ctx, termsClient, agreedTerms)
   162  		if err != nil {
   163  			return errors.Trace(err)
   164  		}
   165  	} else {
   166  		fmt.Fprintf(ctx.Stdout, "You didn't agree to the presented terms.\n")
   167  		return nil
   168  	}
   169  
   170  	return nil
   171  }
   172  
   173  func saveAgreements(ctx *cmd.Context, termsClient api.Client, ts []term) error {
   174  	agreements := make([]wireformat.SaveAgreement, len(ts))
   175  	for i, t := range ts {
   176  		agreements[i] = wireformat.SaveAgreement{
   177  			TermOwner:    t.owner,
   178  			TermName:     t.name,
   179  			TermRevision: t.revision,
   180  		}
   181  	}
   182  	response, err := termsClient.SaveAgreement(&wireformat.SaveAgreements{Agreements: agreements})
   183  	if err != nil {
   184  		return errors.Annotate(err, "failed to save user agreement")
   185  	}
   186  	for _, agreement := range response.Agreements {
   187  		termName := agreement.Term
   188  		if agreement.Owner != "" {
   189  			termName = fmt.Sprintf("%v/%v", agreement.Owner, agreement.Term)
   190  		}
   191  		_, err = fmt.Fprintf(ctx.Stdout, "Agreed to revision %v of %v for Juju users\n", agreement.Revision, termName)
   192  		if err != nil {
   193  			return errors.Trace(err)
   194  		}
   195  	}
   196  	return nil
   197  }
   198  
   199  var userAnswer = func() (string, error) {
   200  	return bufio.NewReader(os.Stdin).ReadString('\n')
   201  }
   202  
   203  func printTerms(ctx *cmd.Context, terms []wireformat.GetTermsResponse) (returnErr error) {
   204  	output := ""
   205  	for _, t := range terms {
   206  		if t.Owner != "" {
   207  			output += fmt.Sprintf(`
   208  === %v/%v/%v: %v ===
   209  %v
   210  ========
   211  `, t.Owner, t.Name, t.Revision, t.CreatedOn, t.Content)
   212  		} else {
   213  			output += fmt.Sprintf(`
   214  === %v/%v: %v ===
   215  %v
   216  ========
   217  `, t.Name, t.Revision, t.CreatedOn, t.Content)
   218  		}
   219  	}
   220  	defer func() {
   221  		if returnErr != nil {
   222  			_, err := fmt.Fprint(ctx.Stdout, output)
   223  			returnErr = errors.Annotate(err, "failed to print plan")
   224  		}
   225  	}()
   226  
   227  	buffer := bytes.NewReader([]byte(output))
   228  	pager, err := pagerCmd()
   229  	if err != nil {
   230  		return err
   231  	}
   232  	pager.Stdout = ctx.Stdout
   233  	pager.Stdin = buffer
   234  	err = pager.Run()
   235  	return errors.Annotate(err, "failed to print plan")
   236  }
   237  
   238  func pagerCmd() (*exec.Cmd, error) {
   239  	os.Unsetenv("LESS")
   240  	if pager := os.Getenv("PAGER"); pager != "" {
   241  		if pagerPath, err := exec.LookPath(pager); err == nil {
   242  			return exec.Command(pagerPath), nil
   243  		}
   244  	}
   245  	if lessPath, err := exec.LookPath("less"); err == nil {
   246  		return exec.Command(lessPath, "-P", "Press 'q' to quit after you've read the terms."), nil
   247  	}
   248  	return nil, errors.NotFoundf("pager")
   249  }
   250  
   251  func userAgrees(input string) bool {
   252  	if input == "y" || input == "Y" || input == "" {
   253  		return true
   254  	}
   255  	return false
   256  }