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  }