Skip to content

Commit c1df24b

Browse files
committed
internal/task: accept secrets explicitly via ExternalConfig struct
Previously, the tweet tasks fetched Twitter API credentials implicitly from the environment, and a dryRun bool parameter was used to disable the task from actually posting a tweet (useful for running tests). This doesn't scale well to being able to supply different credentials, which is needed to be able to run the task in a staging environment, using a staging/test set of credentials. Create an ExternalConfig struct for providing secrets for external services that tasks need to interact with, making this more explicit. This will be used by relui in its upcoming "create tweet" workflows. Update the MailDLCL task analogously to accept the new ExternalConfig parameter, making it more testable and dry-run-capable in the process. Also switch to using *workflow.TaskContext (pointer, not value), since that's what the x/build/internal/workflow package expects. For golang/go#47402. Updates golang/go#47405. Change-Id: I40f0144a57ca0031840fbd1303ab56ac3fefc6c6 Reviewed-on: https://go-review.googlesource.com/c/build/+/382935 Run-TryBot: Dmitri Shuralyov <[email protected]> Trust: Dmitri Shuralyov <[email protected]> Reviewed-by: Alex Rakoczy <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent eb8c3e6 commit c1df24b

File tree

11 files changed

+370
-85
lines changed

11 files changed

+370
-85
lines changed

cmd/releasebot/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,7 @@ The user running a release will need:
1515
* gomote access and a token in your name
1616
* gcloud application default credentials, and an account with GCS access to golang-org for bucket golang-release-staging
1717
* **`release-manager` group membership on Gerrit**
18+
* for `-mode=mail-dl-cl` only, Secret Manager access to Gerrit API secret
19+
* for `-mode=tweet-*` only, Secret Manager access to Twitter API secret
1820

1921
NOTE: all but the Gerrit permission are ensured by the bot on startup.

cmd/releasebot/gerrit.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright 2022 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package main
6+
7+
import (
8+
"context"
9+
10+
"golang.org/x/build/buildenv"
11+
"golang.org/x/build/gerrit"
12+
"golang.org/x/build/internal/secret"
13+
)
14+
15+
const (
16+
// gerritAPIURL is the Gerrit API URL.
17+
gerritAPIURL = "https://go-review.googlesource.com"
18+
)
19+
20+
// loadGerritAuth loads Gerrit API credentials.
21+
func loadGerritAuth() (gerrit.Auth, error) {
22+
sc, err := secret.NewClientInProject(buildenv.Production.ProjectName)
23+
if err != nil {
24+
return nil, err
25+
}
26+
defer sc.Close()
27+
token, err := sc.Retrieve(context.Background(), secret.NameGobotPassword)
28+
if err != nil {
29+
return nil, err
30+
}
31+
return gerrit.BasicAuth("git-gobot.golang.org", token), nil
32+
}

cmd/releasebot/main.go

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -207,28 +207,36 @@ func mailDLCL() {
207207
}
208208
versions := flag.Args()
209209

210-
fmt.Printf("About to create a golang.org/dl CL for the following Go versions:\n\n\t• %s\n\nOk? (Y/n) ", strings.Join(versions, "\n\t• "))
211-
if dryRun {
212-
fmt.Println("dry-run")
213-
return
210+
extCfg := task.ExternalConfig{
211+
DryRun: dryRun,
214212
}
213+
if !dryRun {
214+
extCfg.GerritAPI.URL = gerritAPIURL
215+
var err error
216+
extCfg.GerritAPI.Auth, err = loadGerritAuth()
217+
if err != nil {
218+
log.Fatalln("error loading Gerrit API credentials:", err)
219+
}
220+
}
221+
222+
fmt.Printf("About to create a golang.org/dl CL for the following Go versions:\n\n\t• %s\n\nOk? (Y/n) ", strings.Join(versions, "\n\t• "))
215223
var resp string
216224
if _, err := fmt.Scanln(&resp); err != nil {
217225
log.Fatalln(err)
218226
} else if resp != "Y" && resp != "y" {
219227
log.Fatalln("stopped as requested")
220228
}
221-
changeURL, err := task.MailDLCL(context.Background(), versions)
229+
changeURL, err := task.MailDLCL(&workflow.TaskContext{Context: context.Background(), Logger: log.Default()}, versions, extCfg)
222230
if err != nil {
223-
log.Fatalf(`task.MailDLCL(ctx, %#v) failed:
231+
log.Fatalf(`task.MailDLCL(ctx, %#v, extCfg) failed:
224232
225233
%v
226234
227235
If it's necessary to perform it manually as a workaround,
228236
consider the following steps:
229237
230238
git clone https://go.googlesource.com/dl && cd dl
231-
go run ./internal/genv goX.Y.Z goX.A.B
239+
# create files displayed in the log above
232240
git add .
233241
git commit -m "dl: add goX.Y.Z and goX.A.B"
234242
git codereview mail -trybot -trust
@@ -252,6 +260,17 @@ func postTweet(kind string) {
252260
log.Fatalln("error parsing release tweet JSON object:", err)
253261
}
254262

263+
extCfg := task.ExternalConfig{
264+
DryRun: dryRun,
265+
}
266+
if !dryRun {
267+
var err error
268+
extCfg.TwitterAPI, err = loadTwitterAuth()
269+
if err != nil {
270+
log.Fatalln("error loading Twitter API credentials:", err)
271+
}
272+
}
273+
255274
versions := []string{tweet.Version}
256275
if tweet.SecondaryVersion != "" {
257276
versions = append(versions, tweet.SecondaryVersion+" (secondary)")
@@ -272,20 +291,20 @@ func postTweet(kind string) {
272291
} else if resp != "Y" && resp != "y" {
273292
log.Fatalln("stopped as requested")
274293
}
275-
tweetRelease := map[string]func(workflow.TaskContext, task.ReleaseTweet, bool) (string, error){
294+
tweetRelease := map[string]func(*workflow.TaskContext, task.ReleaseTweet, task.ExternalConfig) (string, error){
276295
"minor": task.TweetMinorRelease,
277296
"beta": task.TweetBetaRelease,
278297
"rc": task.TweetRCRelease,
279298
"major": task.TweetMajorRelease,
280299
}[kind]
281-
tweetURL, err := tweetRelease(workflow.TaskContext{Context: context.Background(), Logger: log.Default()}, tweet, dryRun)
300+
tweetURL, err := tweetRelease(&workflow.TaskContext{Context: context.Background(), Logger: log.Default()}, tweet, extCfg)
282301
if errors.Is(err, task.ErrTweetTooLong) && len([]rune(tweet.Security)) > 120 {
283302
log.Fatalf(`A tweet was not created because it's too long.
284303
285304
The provided security sentence is somewhat long (%d characters),
286305
so try making it shorter to avoid exceeding Twitter's limits.`, len([]rune(tweet.Security)))
287306
} else if err != nil {
288-
log.Fatalf(`tweetRelease(ctx, %#v) failed:
307+
log.Fatalf(`tweetRelease(ctx, %#v, extCfg) failed:
289308
290309
%v
291310

cmd/releasebot/twitter.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright 2022 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package main
6+
7+
import (
8+
"context"
9+
"encoding/json"
10+
11+
"golang.org/x/build/buildenv"
12+
"golang.org/x/build/internal/secret"
13+
)
14+
15+
// loadTwitterAuth loads Twitter API credentials.
16+
func loadTwitterAuth() (secret.TwitterCredentials, error) {
17+
sc, err := secret.NewClientInProject(buildenv.Production.ProjectName)
18+
if err != nil {
19+
return secret.TwitterCredentials{}, err
20+
}
21+
defer sc.Close()
22+
secretJSON, err := sc.Retrieve(context.Background(), secret.NameTwitterAPISecret)
23+
if err != nil {
24+
return secret.TwitterCredentials{}, err
25+
}
26+
var v secret.TwitterCredentials
27+
err = json.Unmarshal([]byte(secretJSON), &v)
28+
if err != nil {
29+
return secret.TwitterCredentials{}, err
30+
}
31+
return v, nil
32+
}

internal/secret/gcp_secret_manager.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package secret
88

99
import (
1010
"context"
11+
"fmt"
1112
"io"
1213
"log"
1314
"path"
@@ -34,7 +35,7 @@ const (
3435
// NameGitHubSSHKey is the secret name for the GitHub SSH private key.
3536
NameGitHubSSHKey = "github-ssh-private-key"
3637

37-
// NameGobotPassword is the secret name for the Gobot password.
38+
// NameGobotPassword is the secret name for the [email protected] Gerrit account password.
3839
NameGobotPassword = "gobot-password"
3940

4041
// NameGomoteSSHPrivateKey is the secret name for the gomote SSH private key.
@@ -66,7 +67,7 @@ const (
6667
// posting tweets from the Go project's Twitter account (twitter.com/golang).
6768
//
6869
// The secret value encodes relevant keys and their secrets as
69-
// a JSON object:
70+
// a JSON object that can be unmarshaled into TwitterCredentials:
7071
//
7172
// {
7273
// "ConsumerKey": "...",
@@ -75,8 +76,31 @@ const (
7576
// "AccessTokenSecret": "..."
7677
// }
7778
NameTwitterAPISecret = "twitter-api-secret"
79+
// NameStagingTwitterAPISecret is the secret name for Twitter API credentials
80+
// for posting tweets using a staging test Twitter account.
81+
//
82+
// This secret is available in the Secret Manager of the x/build staging GCP project.
83+
//
84+
// The secret value encodes relevant keys and their secrets as
85+
// a JSON object that can be unmarshaled into TwitterCredentials.
86+
NameStagingTwitterAPISecret = "staging-" + NameTwitterAPISecret
7887
)
7988

89+
// TwitterCredentials holds Twitter API credentials.
90+
type TwitterCredentials struct {
91+
ConsumerKey string
92+
ConsumerSecret string
93+
AccessTokenKey string
94+
AccessTokenSecret string
95+
}
96+
97+
func (t TwitterCredentials) String() string {
98+
return fmt.Sprintf("{%s (redacted) %s (redacted)}", t.ConsumerKey, t.AccessTokenKey)
99+
}
100+
func (t TwitterCredentials) GoString() string {
101+
return fmt.Sprintf("secret.TwitterCredentials{ConsumerKey:%q ConsumerSecret:(redacted) AccessTokenKey:%q AccessTokenSecret:(redacted)}", t.ConsumerKey, t.AccessTokenKey)
102+
}
103+
80104
type secretClient interface {
81105
AccessSecretVersion(ctx context.Context, req *secretmanagerpb.AccessSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.AccessSecretVersionResponse, error)
82106
io.Closer

internal/task/dlcl.go

Lines changed: 13 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package task
66

77
import (
88
"bytes"
9-
"context"
109
"fmt"
1110
"go/format"
1211
"path"
@@ -15,9 +14,8 @@ import (
1514
"text/template"
1615
"time"
1716

18-
"golang.org/x/build/buildenv"
1917
"golang.org/x/build/gerrit"
20-
"golang.org/x/build/internal/secret"
18+
"golang.org/x/build/internal/workflow"
2119
)
2220

2321
// MailDLCL mails a golang.org/dl CL that adds commands for the
@@ -28,9 +26,8 @@ import (
2826
// • "go1.18" for a major Go release
2927
// • "go1.18beta1" or "go1.18rc1" for a pre-release
3028
//
31-
// Credentials are fetched from Secret Manager.
3229
// On success, the URL of the change is returned, like "https://go.dev/cl/123".
33-
func MailDLCL(ctx context.Context, versions []string) (changeURL string, _ error) {
30+
func MailDLCL(ctx *workflow.TaskContext, versions []string, e ExternalConfig) (changeURL string, _ error) {
3431
if len(versions) < 1 || len(versions) > 2 {
3532
return "", fmt.Errorf("got %d Go versions, want 1 or 2", len(versions))
3633
}
@@ -67,33 +64,36 @@ func MailDLCL(ctx context.Context, versions []string) (changeURL string, _ error
6764
return "", fmt.Errorf("could not gofmt: %v", err)
6865
}
6966
files[path.Join(ver, "main.go")] = string(gofmted)
67+
if log := ctx.Logger; log != nil {
68+
log.Printf("file %q (command %q):\n%s", path.Join(ver, "main.go"), "golang.org/dl/"+ver, gofmted)
69+
}
7070
}
7171

7272
// Create a Gerrit CL using the Gerrit API.
73-
gobot, err := gobot()
74-
if err != nil {
75-
return "", err
73+
if e.DryRun {
74+
return "(dry-run)", nil
7675
}
77-
ci, err := gobot.CreateChange(ctx, gerrit.ChangeInput{
76+
cl := gerrit.NewClient(e.GerritAPI.URL, e.GerritAPI.Auth)
77+
c, err := cl.CreateChange(ctx, gerrit.ChangeInput{
7878
Project: "dl",
7979
Subject: "dl: add " + strings.Join(versions, " and "),
8080
Branch: "master",
8181
})
8282
if err != nil {
8383
return "", err
8484
}
85-
changeID := fmt.Sprintf("%s~%d", ci.Project, ci.ChangeNumber)
85+
changeID := fmt.Sprintf("%s~%d", c.Project, c.ChangeNumber)
8686
for path, content := range files {
87-
err := gobot.ChangeFileContentInChangeEdit(ctx, changeID, path, content)
87+
err := cl.ChangeFileContentInChangeEdit(ctx, changeID, path, content)
8888
if err != nil {
8989
return "", err
9090
}
9191
}
92-
err = gobot.PublishChangeEdit(ctx, changeID)
92+
err = cl.PublishChangeEdit(ctx, changeID)
9393
if err != nil {
9494
return "", err
9595
}
96-
return fmt.Sprintf("https://go.dev/cl/%d", ci.ChangeNumber), nil
96+
return fmt.Sprintf("https://go.dev/cl/%d", c.ChangeNumber), nil
9797
}
9898

9999
func verifyGoVersions(versions ...string) error {
@@ -153,18 +153,3 @@ func main() {
153153
version.Run("{{.Version}}")
154154
}
155155
`))
156-
157-
// gobot creates an authenticated Gerrit API client
158-
// that uses the [email protected] Gerrit account.
159-
func gobot() (*gerrit.Client, error) {
160-
sc, err := secret.NewClientInProject(buildenv.Production.ProjectName)
161-
if err != nil {
162-
return nil, err
163-
}
164-
defer sc.Close()
165-
token, err := sc.Retrieve(context.Background(), secret.NameGobotPassword)
166-
if err != nil {
167-
return nil, err
168-
}
169-
return gerrit.NewClient("https://go-review.googlesource.com", gerrit.BasicAuth("git-gobot.golang.org", token)), nil
170-
}

0 commit comments

Comments
 (0)