go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/bugs/template.go (about) 1 // Copyright 2023 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package bugs 16 17 import ( 18 "strings" 19 "text/template" 20 21 "go.chromium.org/luci/common/errors" 22 ) 23 24 // templateMaxOutputBytes is the maximum number of bytes that a template should 25 // produce. This limit is enforced strictly at config validation time and 26 // loosely (actual size may be 2x larger) at runtime. 27 const templateMaxOutputBytes = 10_000 28 29 // Template is a template for bug comments. 30 type Template struct { 31 template *template.Template 32 } 33 34 // ParseTemplate parses the given template source. 35 func ParseTemplate(t string) (Template, error) { 36 templ, err := template.New("comment_template").Parse(t) 37 if err != nil { 38 return Template{}, err 39 } 40 return Template{templ}, nil 41 } 42 43 // Execute executes the template. 44 func (t *Template) Execute(input TemplateInput) (string, error) { 45 var b strings.Builder 46 err := t.template.Execute(&b, input) 47 if err != nil { 48 return "", errors.Annotate(err, "execute").Err() 49 } 50 if b.Len() > 2*templateMaxOutputBytes { 51 return "", errors.Reason("template produced %v bytes of output, which exceeds the limit of %v bytes", b.Len(), 2*templateMaxOutputBytes).Err() 52 } 53 return b.String(), nil 54 } 55 56 // Validate validates the template. 57 func (t Template) Validate() error { 58 type testCase struct { 59 name string 60 input TemplateInput 61 } 62 testCases := []testCase{ 63 { 64 name: "buganizer", 65 input: TemplateInput{ 66 RuleURL: "https://luci-analysis-deployment/some/url", 67 BugID: TemplateBugID{ 68 id: BugID{ 69 System: BuganizerSystem, 70 ID: "1234567890123", 71 }, 72 }, 73 }, 74 }, { 75 name: "monorail", 76 input: TemplateInput{ 77 RuleURL: "https://luci-analysis-deployment/some/url", 78 BugID: TemplateBugID{ 79 id: BugID{ 80 System: MonorailSystem, 81 ID: "monorailproject/1234567890123", 82 }, 83 }, 84 }, 85 }, 86 { 87 // Reserve the ability to extend to other bug-filing systems; the 88 // template should handle this gracefully. 89 name: "neither buganizer nor monorail", 90 input: TemplateInput{ 91 RuleURL: "https://luci-analysis-deployment/some/url", 92 BugID: TemplateBugID{ 93 id: BugID{ 94 System: "reserved", 95 ID: "reserved", 96 }, 97 }, 98 }, 99 }, 100 } 101 for _, tc := range testCases { 102 var b strings.Builder 103 err := t.template.Execute(&b, tc.input) 104 if err != nil { 105 return errors.Annotate(err, "test case %q", tc.name).Err() 106 } 107 if b.Len() > templateMaxOutputBytes { 108 return errors.Reason("test case %q: template produced %v bytes of output, which exceeds the limit of %v bytes", tc.name, b.Len(), templateMaxOutputBytes).Err() 109 } 110 } 111 return nil 112 } 113 114 // TemplateInput is the input to the policy-specified template for generating 115 // bug comments. 116 type TemplateInput struct { 117 // The link to the LUCI Analysis failure association rule. 118 RuleURL string 119 // The identifier of the bug on which we are commenting. 120 BugID TemplateBugID 121 } 122 123 // NewTemplateBugID initializes a new TemplateBugID. 124 func NewTemplateBugID(id BugID) TemplateBugID { 125 return TemplateBugID{id: id} 126 } 127 128 // TemplateBugID wraps the BugID type so we do not couple the interface the 129 // seen by a project's bug template to our implementation details. 130 // We want full control over the interface the template sees to ensure 131 // project configuration compatibility over time. 132 type TemplateBugID struct { 133 // must remain private. 134 id BugID 135 } 136 137 // IsBuganizer returns whether the bug is a Buganizer bug. 138 func (b TemplateBugID) IsBuganizer() bool { 139 return b.id.System == BuganizerSystem 140 } 141 142 // IsMonorail returns whether the bug is a monorail bug. 143 func (b TemplateBugID) IsMonorail() bool { 144 return b.id.System == MonorailSystem 145 } 146 147 // MonorailProject returns the monorail project for a bug. 148 // (e.g. "chromium" for crbug.com/123456). 149 // Errors if the bug is not a monorail bug. 150 func (b TemplateBugID) MonorailProject() (string, error) { 151 project, _, err := b.id.MonorailProjectAndID() 152 return project, err 153 } 154 155 // MonorailBugID returns the monorail ID for a bug 156 // (e.g. "123456" for crbug.com/123456). 157 // Errors if the bug is not a monorail bug. 158 func (b TemplateBugID) MonorailBugID() (string, error) { 159 _, id, err := b.id.MonorailProjectAndID() 160 return id, err 161 } 162 163 // BuganizerBugID returns the buganizer ID for a bug. 164 // E.g. "123456" for "b/123456". 165 // Errors if the bug is not a buganizer bug. 166 func (b TemplateBugID) BuganizerBugID() (string, error) { 167 if b.id.System != BuganizerSystem { 168 return "", errors.New("not a buganizer bug") 169 } 170 return b.id.ID, nil 171 }