Skip to content

Commit 06d3137

Browse files
committed
feat: add new tool to send patch release cherry pick notificatications
Signed-off-by: Carlos Panato <[email protected]>
1 parent c616f45 commit 06d3137

File tree

4 files changed

+352
-0
lines changed

4 files changed

+352
-0
lines changed

cmd/patch-release-notify/cmd/root.go

+294
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
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+
"fmt"
23+
"html/template"
24+
"io"
25+
"math"
26+
"net/http"
27+
"os"
28+
"path/filepath"
29+
"strings"
30+
"time"
31+
32+
"github.com/pkg/errors"
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
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 errors.Wrap(err, "validating schedule-path options")
163+
}
164+
165+
if opts.sendgridAPIKey == "" {
166+
return errors.Errorf(
167+
"$%s is not set", sendgridAPIKeyEnvKey,
168+
)
169+
}
170+
171+
data, err := loadFileOrURL(opts.schedulePath)
172+
if err != nil {
173+
return errors.Wrap(err, "failed to read the file")
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 errors.Wrap(err, "failed to decode the file")
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 errors.Wrap(err, "parsing schedule time")
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 errors.Wrap(err, "parsing template")
209+
}
210+
211+
var tmplBytes bytes.Buffer
212+
err = tmpl.Execute(&tmplBytes, output)
213+
if err != nil {
214+
return errors.Wrap(err, "parsing values to the template")
215+
}
216+
217+
if shouldSendEmail {
218+
if !opts.nomock {
219+
logrus.Info("This is a mock only, will print out the email before sending to a test mailing list")
220+
fmt.Println(tmplBytes.String())
221+
}
222+
223+
logrus.Info("Preparing mail sender")
224+
m := mail.NewSender(opts.sendgridAPIKey)
225+
226+
if opts.name != "" && opts.email != "" {
227+
if err := m.SetSender(opts.name, opts.email); err != nil {
228+
return errors.Wrap(err, "unable to set mail sender")
229+
}
230+
} else {
231+
logrus.Info("Retrieving default sender from sendgrid API")
232+
if err := m.SetDefaultSender(); err != nil {
233+
return errors.Wrap(err, "setting default sender")
234+
}
235+
}
236+
237+
groups := []mail.GoogleGroup{mail.KubernetesAnnounceTestGoogleGroup}
238+
if opts.nomock {
239+
groups = []mail.GoogleGroup{
240+
mail.KubernetesDevGoogleGroup,
241+
}
242+
}
243+
logrus.Infof("Using Google Groups as announcement target: %v", groups)
244+
245+
if err := m.SetGoogleGroupRecipients(groups...); err != nil {
246+
return errors.Wrap(err, "unable to set mail recipients")
247+
}
248+
249+
logrus.Info("Sending mail")
250+
subject := "[Please Read] Patch Releases cherry-pick deadline"
251+
252+
if err := m.Send(tmplBytes.String(), subject); err != nil {
253+
return errors.Wrap(err, "unable to send mail")
254+
}
255+
} else {
256+
logrus.Info("No email is needed to send")
257+
}
258+
259+
return nil
260+
}
261+
262+
// SetAndValidate sets some default options and verifies if options are valid
263+
func (o *options) SetAndValidate() error {
264+
logrus.Info("Validating schedule-path options...")
265+
266+
if o.schedulePath == "" {
267+
return errors.Errorf("need to set the schedule-path")
268+
}
269+
270+
return nil
271+
}
272+
273+
func loadFileOrURL(fileRef string) ([]byte, error) {
274+
var raw []byte
275+
var err error
276+
if strings.HasPrefix(fileRef, "http://") || strings.HasPrefix(fileRef, "https://") {
277+
// #nosec G107
278+
resp, err := http.Get(fileRef)
279+
if err != nil {
280+
return nil, err
281+
}
282+
defer resp.Body.Close()
283+
raw, err = io.ReadAll(resp.Body)
284+
if err != nil {
285+
return nil, err
286+
}
287+
} else {
288+
raw, err = os.ReadFile(filepath.Clean(fileRef))
289+
if err != nil {
290+
return nil, err
291+
}
292+
}
293+
return raw, nil
294+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
Hello Kubernetes Community!
2+
3+
{{range .Releases}}
4+
The cherry-pick deadline for the {{ .Release }} branches is {{ .CherryPickDeadline }} EOD PT.
5+
{{end}}
6+
7+
Here are some quick links to search for cherry-pick PRs:
8+
9+
{{range .Releases}}
10+
- release-{{ .Release }}: https://github.com/kubernetes/kubernetes/pulls?q=is%3Apr+is%3Aopen+base%3Arelease-{{ .Release }}+label%3Ado-not-merge%2Fcherry-pick-not-approved
11+
{{end}}
12+
13+
14+
For PRs that you intend to land for the upcoming patch sets, please
15+
ensure they have:
16+
- a release note in the PR description
17+
- /sig
18+
- /kind
19+
- /priority
20+
- /lgtm
21+
- /approve
22+
- passing tests
23+
24+
Details on the cherry-pick process can be found here:
25+
https://git.k8s.io/community/contributors/devel/sig-release/cherry-picks.md
26+
27+
We keep general info and up-to-date timelines for patch releases here:
28+
https://kubernetes.io/releases/patch-releases/#upcoming-monthly-releases
29+
30+
If you have any questions for the Release Managers, please feel free to
31+
reach out to us at #release-management (Kubernetes Slack) or [email protected]
32+
33+
We wish everyone a happy and safe week!
34+
SIG-Release Team

cmd/patch-release-notify/main.go

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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 main
18+
19+
import "k8s.io/release/cmd/patch-release-notify/cmd"
20+
21+
func main() {
22+
cmd.Execute()
23+
}

compile-release-tools

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ RELEASE_TOOLS=(
2222
krel
2323
kubepkg
2424
schedule-builder
25+
patch-release-notify
2526
)
2627

2728
setup_env() {

0 commit comments

Comments
 (0)