Permalink
Cannot retrieve contributors at this time
Fetching contributors…
| // Copyright 2016 Canonical Ltd. | |
| // Licensed under the AGPLv3, see LICENCE file for details. | |
| package agree | |
| import ( | |
| "bufio" | |
| "bytes" | |
| "fmt" | |
| "os" | |
| "os/exec" | |
| "strings" | |
| "github.com/juju/cmd" | |
| "github.com/juju/errors" | |
| "github.com/juju/gnuflag" | |
| "github.com/juju/juju/cmd/modelcmd" | |
| "github.com/juju/terms-client/api" | |
| "github.com/juju/terms-client/api/wireformat" | |
| "gopkg.in/juju/charm.v6-unstable" | |
| ) | |
| var ( | |
| clientNew = api.NewClient | |
| ) | |
| const agreeDoc = ` | |
| Agree to the terms required by a charm. | |
| When deploying a charm that requires agreement to terms, use 'juju agree' to | |
| view the terms and agree to them. Then the charm may be deployed. | |
| Once you have agreed to terms, you will not be prompted to view them again. | |
| Examples: | |
| # Displays terms for somePlan revision 1 and prompts for agreement. | |
| juju agree somePlan/1 | |
| # Displays the terms for revision 1 of somePlan, revision 2 of otherPlan, | |
| # and prompts for agreement. | |
| juju agree somePlan/1 otherPlan/2 | |
| # Agrees to the terms without prompting. | |
| juju agree somePlan/1 otherPlan/2 --yes | |
| ` | |
| // NewAgreeCommand returns a new command that can be | |
| // used to create user agreements. | |
| func NewAgreeCommand() cmd.Command { | |
| return &agreeCommand{} | |
| } | |
| type term struct { | |
| owner string | |
| name string | |
| revision int | |
| } | |
| // agreeCommand creates a user agreement to the specified terms. | |
| type agreeCommand struct { | |
| modelcmd.JujuCommandBase | |
| terms []term | |
| termIds []string | |
| SkipTermContent bool | |
| } | |
| // SetFlags implements Command.SetFlags. | |
| func (c *agreeCommand) SetFlags(f *gnuflag.FlagSet) { | |
| c.JujuCommandBase.SetFlags(f) | |
| f.BoolVar(&c.SkipTermContent, "yes", false, "Agree to terms non interactively") | |
| } | |
| // Info implements Command.Info. | |
| func (c *agreeCommand) Info() *cmd.Info { | |
| return &cmd.Info{ | |
| Name: "agree", | |
| Args: "<term>", | |
| Purpose: "Agree to terms.", | |
| Doc: agreeDoc, | |
| } | |
| } | |
| // Init read and verifies the arguments. | |
| func (c *agreeCommand) Init(args []string) error { | |
| if len(args) < 1 { | |
| return errors.New("missing arguments") | |
| } | |
| for _, t := range args { | |
| termId, err := charm.ParseTerm(t) | |
| if err != nil { | |
| return errors.Annotate(err, "invalid term format") | |
| } | |
| if termId.Revision == 0 { | |
| return errors.Errorf("must specify a valid term revision %q", t) | |
| } | |
| c.terms = append(c.terms, term{owner: termId.Owner, name: termId.Name, revision: termId.Revision}) | |
| c.termIds = append(c.termIds, t) | |
| } | |
| if len(c.terms) == 0 { | |
| return errors.New("must specify a valid term revision") | |
| } | |
| return nil | |
| } | |
| // Run implements Command.Run. | |
| func (c *agreeCommand) Run(ctx *cmd.Context) error { | |
| client, err := c.BakeryClient() | |
| if err != nil { | |
| return errors.Trace(err) | |
| } | |
| termsClient, err := clientNew(api.HTTPClient(client)) | |
| if err != nil { | |
| return err | |
| } | |
| if c.SkipTermContent { | |
| err := saveAgreements(ctx, termsClient, c.terms) | |
| if err != nil { | |
| return errors.Trace(err) | |
| } | |
| return nil | |
| } | |
| needAgreement := []wireformat.GetTermsResponse{} | |
| terms, err := termsClient.GetUnsignedTerms(&wireformat.CheckAgreementsRequest{ | |
| Terms: c.termIds, | |
| }) | |
| if err != nil { | |
| return errors.Annotate(err, "failed to retrieve terms") | |
| } | |
| needAgreement = append(needAgreement, terms...) | |
| if len(needAgreement) == 0 { | |
| fmt.Fprintf(ctx.Stdout, "Already agreed\n") | |
| return nil | |
| } | |
| err = printTerms(ctx, needAgreement) | |
| if err != nil { | |
| return errors.Trace(err) | |
| } | |
| fmt.Fprintf(ctx.Stdout, "Do you agree to the displayed terms? (Y/n): ") | |
| answer, err := userAnswer() | |
| if err != nil { | |
| return errors.Trace(err) | |
| } | |
| agreedTerms := make([]term, len(needAgreement)) | |
| for i, t := range needAgreement { | |
| agreedTerms[i] = term{owner: t.Owner, name: t.Name, revision: t.Revision} | |
| } | |
| answer = strings.TrimSpace(answer) | |
| if userAgrees(answer) { | |
| err = saveAgreements(ctx, termsClient, agreedTerms) | |
| if err != nil { | |
| return errors.Trace(err) | |
| } | |
| } else { | |
| fmt.Fprintf(ctx.Stdout, "You didn't agree to the presented terms.\n") | |
| return nil | |
| } | |
| return nil | |
| } | |
| func saveAgreements(ctx *cmd.Context, termsClient api.Client, ts []term) error { | |
| agreements := make([]wireformat.SaveAgreement, len(ts)) | |
| for i, t := range ts { | |
| agreements[i] = wireformat.SaveAgreement{ | |
| TermOwner: t.owner, | |
| TermName: t.name, | |
| TermRevision: t.revision, | |
| } | |
| } | |
| response, err := termsClient.SaveAgreement(&wireformat.SaveAgreements{Agreements: agreements}) | |
| if err != nil { | |
| return errors.Annotate(err, "failed to save user agreement") | |
| } | |
| for _, agreement := range response.Agreements { | |
| termName := agreement.Term | |
| if agreement.Owner != "" { | |
| termName = fmt.Sprintf("%v/%v", agreement.Owner, agreement.Term) | |
| } | |
| _, err = fmt.Fprintf(ctx.Stdout, "Agreed to revision %v of %v for Juju users\n", agreement.Revision, termName) | |
| if err != nil { | |
| return errors.Trace(err) | |
| } | |
| } | |
| return nil | |
| } | |
| var userAnswer = func() (string, error) { | |
| return bufio.NewReader(os.Stdin).ReadString('\n') | |
| } | |
| func printTerms(ctx *cmd.Context, terms []wireformat.GetTermsResponse) (returnErr error) { | |
| output := "" | |
| for _, t := range terms { | |
| if t.Owner != "" { | |
| output += fmt.Sprintf(` | |
| === %v/%v/%v: %v === | |
| %v | |
| ======== | |
| `, t.Owner, t.Name, t.Revision, t.CreatedOn, t.Content) | |
| } else { | |
| output += fmt.Sprintf(` | |
| === %v/%v: %v === | |
| %v | |
| ======== | |
| `, t.Name, t.Revision, t.CreatedOn, t.Content) | |
| } | |
| } | |
| defer func() { | |
| if returnErr != nil { | |
| _, err := fmt.Fprint(ctx.Stdout, output) | |
| returnErr = errors.Annotate(err, "failed to print plan") | |
| } | |
| }() | |
| buffer := bytes.NewReader([]byte(output)) | |
| pager, err := pagerCmd() | |
| if err != nil { | |
| return err | |
| } | |
| pager.Stdout = ctx.Stdout | |
| pager.Stdin = buffer | |
| err = pager.Run() | |
| return errors.Annotate(err, "failed to print plan") | |
| } | |
| func pagerCmd() (*exec.Cmd, error) { | |
| os.Unsetenv("LESS") | |
| if pager := os.Getenv("PAGER"); pager != "" { | |
| if pagerPath, err := exec.LookPath(pager); err == nil { | |
| return exec.Command(pagerPath), nil | |
| } | |
| } | |
| if lessPath, err := exec.LookPath("less"); err == nil { | |
| return exec.Command(lessPath, "-P", "Press 'q' to quit after you've read the terms."), nil | |
| } | |
| return nil, errors.NotFoundf("pager") | |
| } | |
| func userAgrees(input string) bool { | |
| if input == "y" || input == "Y" || input == "" { | |
| return true | |
| } | |
| return false | |
| } |