Skip to content

Commit 20c71dc

Browse files
committed
add aws lambda function to send patch cherry pick notification
Signed-off-by: cpanato <[email protected]>
1 parent f294861 commit 20c71dc

File tree

14 files changed

+1219
-16
lines changed

14 files changed

+1219
-16
lines changed
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Patch Release Notification
2+
3+
This simple tool has the objective to send an notification email when we are closer to
4+
the patch release cycle to let people know that the cherry pick deadline is approaching.
+295
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
/*
2+
Copyright 2022 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cmd
18+
19+
import (
20+
"bytes"
21+
"embed"
22+
"errors"
23+
"fmt"
24+
"html/template"
25+
"io"
26+
"math"
27+
"net/http"
28+
"os"
29+
"path/filepath"
30+
"strings"
31+
"time"
32+
33+
"github.com/sirupsen/logrus"
34+
"github.com/spf13/cobra"
35+
36+
"k8s.io/release/cmd/schedule-builder/model"
37+
"k8s.io/release/pkg/mail"
38+
"sigs.k8s.io/release-utils/env"
39+
"sigs.k8s.io/release-utils/log"
40+
"sigs.k8s.io/yaml"
41+
)
42+
43+
//go:embed templates/*.tmpl
44+
var tpls embed.FS
45+
46+
// rootCmd represents the base command when called without any subcommands
47+
var rootCmd = &cobra.Command{
48+
Use: "patch-release-notify --schedule-path /path/to/schedule.yaml",
49+
Short: "patch-release-notify check the cherry pick deadline and send an email to notify",
50+
Example: "patch-release-notify --schedule-path /path/to/schedule.yaml",
51+
SilenceUsage: true,
52+
SilenceErrors: true,
53+
PersistentPreRunE: initLogging,
54+
RunE: func(*cobra.Command, []string) error {
55+
return run(opts)
56+
},
57+
}
58+
59+
type options struct {
60+
sendgridAPIKey string
61+
schedulePath string
62+
dayToalert int
63+
name string
64+
email string
65+
nomock bool
66+
logLevel string
67+
}
68+
69+
var opts = &options{}
70+
71+
const (
72+
sendgridAPIKeyEnvKey = "SENDGRID_API_KEY" //nolint:gosec // this will be provided via env vars
73+
layout = "2006-01-02"
74+
75+
schedulePathFlag = "schedule-path"
76+
nameFlag = "name"
77+
emailFlag = "email"
78+
dayToalertFlag = "days-to-alert"
79+
)
80+
81+
var requiredFlags = []string{
82+
schedulePathFlag,
83+
}
84+
85+
type Template struct {
86+
Releases []TemplateRelease
87+
}
88+
89+
type TemplateRelease struct {
90+
Release string
91+
CherryPickDeadline string
92+
}
93+
94+
// Execute adds all child commands to the root command and sets flags appropriately.
95+
// This is called by main.main(). It only needs to happen once to the rootCmd.
96+
func Execute() {
97+
if err := rootCmd.Execute(); err != nil {
98+
logrus.Fatal(err)
99+
}
100+
}
101+
102+
func init() {
103+
opts.sendgridAPIKey = env.Default(sendgridAPIKeyEnvKey, "")
104+
105+
rootCmd.PersistentFlags().StringVar(
106+
&opts.schedulePath,
107+
schedulePathFlag,
108+
"",
109+
"path where can find the schedule.yaml file",
110+
)
111+
112+
rootCmd.PersistentFlags().BoolVar(
113+
&opts.nomock,
114+
"nomock",
115+
false,
116+
"run the command to target the production environment",
117+
)
118+
119+
rootCmd.PersistentFlags().StringVar(
120+
&opts.logLevel,
121+
"log-level",
122+
"info",
123+
fmt.Sprintf("the logging verbosity, either %s", log.LevelNames()),
124+
)
125+
126+
rootCmd.PersistentFlags().StringVarP(
127+
&opts.name,
128+
nameFlag,
129+
"n",
130+
"",
131+
"mail sender name",
132+
)
133+
134+
rootCmd.PersistentFlags().IntVar(
135+
&opts.dayToalert,
136+
dayToalertFlag,
137+
3,
138+
"day to before the deadline to send the notification. Defaults to 3 days.",
139+
)
140+
141+
rootCmd.PersistentFlags().StringVarP(
142+
&opts.email,
143+
emailFlag,
144+
"e",
145+
"",
146+
"email address",
147+
)
148+
149+
for _, flag := range requiredFlags {
150+
if err := rootCmd.MarkPersistentFlagRequired(flag); err != nil {
151+
logrus.Fatal(err)
152+
}
153+
}
154+
}
155+
156+
func initLogging(*cobra.Command, []string) error {
157+
return log.SetupGlobalLogger(opts.logLevel)
158+
}
159+
160+
func run(opts *options) error {
161+
if err := opts.SetAndValidate(); err != nil {
162+
return fmt.Errorf("validating schedule-path options: %w", err)
163+
}
164+
165+
if opts.sendgridAPIKey == "" {
166+
return fmt.Errorf(
167+
"$%s is not set", sendgridAPIKeyEnvKey,
168+
)
169+
}
170+
171+
data, err := loadFileOrURL(opts.schedulePath)
172+
if err != nil {
173+
return fmt.Errorf("failed to read the file: %w", err)
174+
}
175+
176+
patchSchedule := &model.PatchSchedule{}
177+
178+
logrus.Info("Parsing the schedule...")
179+
180+
if err := yaml.UnmarshalStrict(data, &patchSchedule); err != nil {
181+
return fmt.Errorf("failed to decode the file: %w", err)
182+
}
183+
184+
output := &Template{}
185+
186+
shouldSendEmail := false
187+
188+
for _, patch := range patchSchedule.Schedules {
189+
t, err := time.Parse(layout, patch.CherryPickDeadline)
190+
if err != nil {
191+
return fmt.Errorf("parsing schedule time: %w", err)
192+
}
193+
194+
currentTime := time.Now().UTC()
195+
days := t.Sub(currentTime).Hours() / 24
196+
intDay, _ := math.Modf(days)
197+
if int(intDay) == opts.dayToalert {
198+
output.Releases = append(output.Releases, TemplateRelease{
199+
Release: patch.Release,
200+
CherryPickDeadline: patch.CherryPickDeadline,
201+
})
202+
shouldSendEmail = true
203+
}
204+
}
205+
206+
tmpl, err := template.ParseFS(tpls, "templates/email.tmpl")
207+
if err != nil {
208+
return fmt.Errorf("parsing template: %w", err)
209+
}
210+
211+
var tmplBytes bytes.Buffer
212+
err = tmpl.Execute(&tmplBytes, output)
213+
if err != nil {
214+
return fmt.Errorf("parsing values to the template: %w", err)
215+
}
216+
217+
if !shouldSendEmail {
218+
logrus.Info("No email is needed to send")
219+
return nil
220+
}
221+
222+
if !opts.nomock {
223+
logrus.Info("This is a mock only, will print out the email before sending to a test mailing list")
224+
fmt.Println(tmplBytes.String())
225+
}
226+
227+
logrus.Info("Preparing mail sender")
228+
m := mail.NewSender(opts.sendgridAPIKey)
229+
230+
if opts.name != "" && opts.email != "" {
231+
if err := m.SetSender(opts.name, opts.email); err != nil {
232+
return fmt.Errorf("unable to set mail sender: %w", err)
233+
}
234+
} else {
235+
logrus.Info("Retrieving default sender from sendgrid API")
236+
if err := m.SetDefaultSender(); err != nil {
237+
return fmt.Errorf("setting default sender: %w", err)
238+
}
239+
}
240+
241+
groups := []mail.GoogleGroup{mail.KubernetesAnnounceTestGoogleGroup}
242+
if opts.nomock {
243+
groups = []mail.GoogleGroup{
244+
mail.KubernetesDevGoogleGroup,
245+
}
246+
}
247+
logrus.Infof("Using Google Groups as announcement target: %v", groups)
248+
249+
if err := m.SetGoogleGroupRecipients(groups...); err != nil {
250+
return fmt.Errorf("unable to set mail recipients: %w", err)
251+
}
252+
253+
logrus.Info("Sending mail")
254+
subject := "[Please Read] Patch Releases cherry-pick deadline"
255+
256+
if err := m.Send(tmplBytes.String(), subject); err != nil {
257+
return fmt.Errorf("unable to send mail: %w", err)
258+
}
259+
260+
return nil
261+
}
262+
263+
// SetAndValidate sets some default options and verifies if options are valid
264+
func (o *options) SetAndValidate() error {
265+
logrus.Info("Validating schedule-path options...")
266+
267+
if o.schedulePath == "" {
268+
return errors.New("need to set the schedule-path")
269+
}
270+
271+
return nil
272+
}
273+
274+
func loadFileOrURL(fileRef string) ([]byte, error) {
275+
var raw []byte
276+
var err error
277+
if strings.HasPrefix(fileRef, "http://") || strings.HasPrefix(fileRef, "https://") {
278+
// #nosec G107
279+
resp, err := http.Get(fileRef)
280+
if err != nil {
281+
return nil, err
282+
}
283+
defer resp.Body.Close()
284+
raw, err = io.ReadAll(resp.Body)
285+
if err != nil {
286+
return nil, err
287+
}
288+
} else {
289+
raw, err = os.ReadFile(filepath.Clean(fileRef))
290+
if err != nil {
291+
return nil, err
292+
}
293+
}
294+
return raw, nil
295+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<body>
4+
<p>Hello Kubernetes Community!</p>
5+
{{range .Releases}}
6+
<p>The cherry-pick deadline for the <b>{{ .Release }}</b> branches is <b>{{ .CherryPickDeadline }} EOD PT.</b></p>
7+
{{end}}
8+
<br><p>Here are some quick links to search for cherry-pick PRs:</p>
9+
{{range .Releases}}
10+
<p> - release-{{ .Release }}: https://github.com/kubernetes/kubernetes/pulls?q=is%3Apr+is%3Aopen+base%3Arelease-{{ .Release }}+label%3Ado-not-merge%2Fcherry-pick-not-approved</p>
11+
{{end}}
12+
<br>
13+
<p>For PRs that you intend to land for the upcoming patch sets, please
14+
ensure they have:</p>
15+
<p> - a release note in the PR description</p>
16+
<p> - /sig</p>
17+
<p> - /kind</p>
18+
<p> - /priority</p>
19+
<p> - /lgtm</p>
20+
<p> - /approve</p>
21+
<p> - passing tests</p>
22+
<br>
23+
<p>Details on the cherry-pick process can be found here:</p>
24+
<p>https://git.k8s.io/community/contributors/devel/sig-release/cherry-picks.md</p>
25+
<p>We keep general info and up-to-date timelines for patch releases here:</p>
26+
<p>https://kubernetes.io/releases/patch-releases/#upcoming-monthly-releases</p>
27+
<p>If you have any questions for the Release Managers, please feel free to
28+
reach out to us at #release-management (Kubernetes Slack) or [email protected]</p><br>
29+
<p>We wish everyone a happy and safe week!</p>
30+
<p>SIG-Release Team</p>
31+
</body>
32+
</html>

0 commit comments

Comments
 (0)