blob: 19c5add477c602f540ee18f5ccc34ba8335a6fe2 [file] [log] [blame]
// Copyright 2023 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package bugs
import (
"strings"
"text/template"
"go.chromium.org/luci/common/errors"
)
// templateMaxOutputBytes is the maximum number of bytes that a template should
// produce. This limit is enforced strictly at config validation time and
// loosely (actual size may be 2x larger) at runtime.
const templateMaxOutputBytes = 10_000
// Template is a template for bug comments.
type Template struct {
template *template.Template
}
// ParseTemplate parses the given template source.
func ParseTemplate(t string) (Template, error) {
templ, err := template.New("comment_template").Parse(t)
if err != nil {
return Template{}, err
}
return Template{templ}, nil
}
// Execute executes the template.
func (t *Template) Execute(input TemplateInput) (string, error) {
var b strings.Builder
err := t.template.Execute(&b, input)
if err != nil {
return "", errors.Annotate(err, "execute").Err()
}
if b.Len() > 2*templateMaxOutputBytes {
return "", errors.Reason("template produced %v bytes of output, which exceeds the limit of %v bytes", b.Len(), 2*templateMaxOutputBytes).Err()
}
return b.String(), nil
}
// Validate validates the template.
func (t Template) Validate() error {
type testCase struct {
name string
input TemplateInput
}
testCases := []testCase{
{
name: "buganizer",
input: TemplateInput{
RuleURL: "https://luci-analysis-deployment/some/url",
BugID: TemplateBugID{
id: BugID{
System: BuganizerSystem,
ID: "1234567890123",
},
},
},
}, {
name: "monorail",
input: TemplateInput{
RuleURL: "https://luci-analysis-deployment/some/url",
BugID: TemplateBugID{
id: BugID{
System: MonorailSystem,
ID: "monorailproject/1234567890123",
},
},
},
},
{
// Reserve the ability to extend to other bug-filing systems; the
// template should handle this gracefully.
name: "neither buganizer nor monorail",
input: TemplateInput{
RuleURL: "https://luci-analysis-deployment/some/url",
BugID: TemplateBugID{
id: BugID{
System: "reserved",
ID: "reserved",
},
},
},
},
}
for _, tc := range testCases {
var b strings.Builder
err := t.template.Execute(&b, tc.input)
if err != nil {
return errors.Annotate(err, "test case %q", tc.name).Err()
}
if b.Len() > templateMaxOutputBytes {
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()
}
}
return nil
}
// TemplateInput is the input to the policy-specified template for generating
// bug comments.
type TemplateInput struct {
// The link to the LUCI Analysis failure association rule.
RuleURL string
// The identifier of the bug on which we are commenting.
BugID TemplateBugID
}
// NewTemplateBugID initializes a new TemplateBugID.
func NewTemplateBugID(id BugID) TemplateBugID {
return TemplateBugID{id: id}
}
// TemplateBugID wraps the BugID type so we do not couple the interface the
// seen by a project's bug template to our implementation details.
// We want full control over the interface the template sees to ensure
// project configuration compatibility over time.
type TemplateBugID struct {
// must remain private.
id BugID
}
// IsBuganizer returns whether the bug is a Buganizer bug.
func (b TemplateBugID) IsBuganizer() bool {
return b.id.System == BuganizerSystem
}
// IsMonorail returns whether the bug is a monorail bug.
func (b TemplateBugID) IsMonorail() bool {
return b.id.System == MonorailSystem
}
// MonorailProject returns the monorail project for a bug.
// (e.g. "chromium" for crbug.com/123456).
// Errors if the bug is not a monorail bug.
func (b TemplateBugID) MonorailProject() (string, error) {
project, _, err := b.id.MonorailProjectAndID()
return project, err
}
// MonorailBugID returns the monorail ID for a bug
// (e.g. "123456" for crbug.com/123456).
// Errors if the bug is not a monorail bug.
func (b TemplateBugID) MonorailBugID() (string, error) {
_, id, err := b.id.MonorailProjectAndID()
return id, err
}
// BuganizerBugID returns the buganizer ID for a bug.
// E.g. "123456" for "b/123456".
// Errors if the bug is not a buganizer bug.
func (b TemplateBugID) BuganizerBugID() (string, error) {
if b.id.System != BuganizerSystem {
return "", errors.New("not a buganizer bug")
}
return b.id.ID, nil
}