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 }