Skip to content

Commit be70916

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 be70916

File tree

4 files changed

+352
-0
lines changed

4 files changed

+352
-0
lines changed
Lines changed: 294 additions & 0 deletions
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+
"mock",
115+
false,
116+
fmt.Sprintf("check the email without sending it"),
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+
var (
177+
patchSchedule model.PatchSchedule
178+
)
179+
180+
logrus.Info("Parsing the schedule...")
181+
182+
if err := yaml.UnmarshalStrict(data, &patchSchedule); err != nil {
183+
return errors.Wrap(err, "failed to decode the file")
184+
}
185+
186+
output := &Template{}
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+
} else {
203+
output.Releases = append(output.Releases, TemplateRelease{
204+
Release: patch.Release,
205+
CherryPickDeadline: patch.CherryPickDeadline,
206+
})
207+
}
208+
}
209+
210+
tmpl, err := template.ParseFS(tpls, "templates/email.tmpl")
211+
if err != nil {
212+
return errors.Wrap(err, "parsing template")
213+
}
214+
215+
var tmplBytes bytes.Buffer
216+
err = tmpl.Execute(&tmplBytes, output)
217+
if err != nil {
218+
return errors.Wrap(err, "parsing values to the template")
219+
}
220+
221+
if !opts.nomock {
222+
logrus.Info("This is a mock only, will print out the email before sending to a test mailing list")
223+
fmt.Println(tmplBytes.String())
224+
}
225+
226+
logrus.Info("Preparing mail sender")
227+
m := mail.NewSender(opts.sendgridAPIKey)
228+
229+
if opts.name != "" && opts.email != "" {
230+
if err := m.SetSender(opts.name, opts.email); err != nil {
231+
return errors.Wrap(err, "unable to set mail sender")
232+
}
233+
} else {
234+
logrus.Info("Retrieving default sender from sendgrid API")
235+
if err := m.SetDefaultSender(); err != nil {
236+
return errors.Wrap(err, "setting default sender")
237+
}
238+
}
239+
240+
groups := []mail.GoogleGroup{mail.KubernetesAnnounceTestGoogleGroup}
241+
if opts.nomock {
242+
groups = []mail.GoogleGroup{
243+
mail.KubernetesDevGoogleGroup,
244+
}
245+
}
246+
logrus.Infof("Using Google Groups as announcement target: %v", groups)
247+
248+
if err := m.SetGoogleGroupRecipients(groups...); err != nil {
249+
return errors.Wrap(err, "unable to set mail recipients")
250+
}
251+
252+
logrus.Info("Sending mail")
253+
subject := "[Please Read] Patch Releases cherry-pick deadline"
254+
255+
if err := m.Send(tmplBytes.String(), subject); err != nil {
256+
return errors.Wrap(err, "unable to send mail")
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+
}
Lines changed: 34 additions & 0 deletions
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

Lines changed: 23 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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)