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