Skip to content

Commit 90e0f6e

Browse files
authored
feat: add update-image CLI command (#2580)
Adds new `librarian update-image` CLI command. Towards #2342 Fixes #2546
1 parent 5d9e741 commit 90e0f6e

File tree

9 files changed

+1153
-41
lines changed

9 files changed

+1153
-41
lines changed

cmd/librarian/doc.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,67 @@ Flags:
269269
is configured as a language repository.
270270
-v enables verbose logging
271271
272+
# update-image
273+
274+
The 'update-image' command is used to update the 'image' SHA
275+
of the language container for a language repository.
276+
277+
This command's primary responsibilities are to:
278+
279+
- Update the 'image' field in '.librarian/state.yaml'
280+
- Regenerate each library with the new language container using googleapis'
281+
proto definitions at the 'last_generated_commit'
282+
283+
Examples:
284+
285+
# Create a PR that updates the language container to latest image.
286+
librarian update-image --commit --push
287+
288+
# Create a PR that updates the language container to the specified image.
289+
librarian update-image --commit --push --image=<some-image-with-sha>
290+
291+
Usage:
292+
293+
librarian update-image [flags]
294+
295+
Flags:
296+
297+
-api-source string
298+
The location of an API specification repository.
299+
Can be a remote URL or a local file path. (default "https://github.com/googleapis/googleapis")
300+
-branch string
301+
The branch to use with remote code repositories. This is used to specify
302+
which branch to clone and which branch to use as the base for a pull
303+
request. (default "main")
304+
-build
305+
If true, Librarian will build each generated library by invoking the
306+
language-specific container.
307+
-commit
308+
If true, librarian will create a commit for the change but not create
309+
a pull request. This flag is ignored if push is set to true.
310+
-host-mount string
311+
For use when librarian is running in a container. A mapping of a
312+
directory from the host to the container, in the format
313+
<host-mount>:<local-mount>.
314+
-image string
315+
Language specific image used to invoke code generation and releasing.
316+
If not specified, the image configured in the state.yaml is used.
317+
-output string
318+
Working directory root. When this is not specified, a working directory
319+
will be created in /tmp.
320+
-push
321+
If true, Librarian will create a commit,
322+
push and create a pull request for the changes.
323+
A GitHub token with push access must be provided via the
324+
LIBRARIAN_GITHUB_TOKEN environment variable.
325+
-repo string
326+
Code repository where the generated code will reside. Can be a remote
327+
in the format of a remote URL such as https://github.com/{owner}/{repo} or a
328+
local file path like /path/to/repo. Both absolute and relative paths are
329+
supported. If not specified, will try to detect if the current working directory
330+
is configured as a language repository.
331+
-v enables verbose logging
332+
272333
# version
273334
274335
Version prints version information for the librarian binary.

internal/librarian/command.go

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ const (
5353
pullRequestOnboard
5454
pullRequestGenerate
5555
pullRequestRelease
56+
pullRequestUpdateImage
5657
)
5758

5859
// String returns the string representation of a pullRequestType.
@@ -63,6 +64,7 @@ func (t pullRequestType) String() string {
6364
pullRequestOnboard: "onboard",
6465
pullRequestGenerate: "generate",
6566
pullRequestRelease: "release",
67+
pullRequestUpdateImage: "update image",
6668
}
6769
if name, ok := names[t]; ok {
6870
return name
@@ -97,17 +99,29 @@ type ContainerClient interface {
9799
}
98100

99101
type commitInfo struct {
100-
branch string
101-
commit bool
102-
commitMessage string
103-
ghClient GitHubClient
104-
prType pullRequestType
102+
// branch is the base branch of the created pull request.
103+
branch string
104+
// commit declares whether or not to create a commit.
105+
commit bool
106+
// commitMessage is used as the message on the actual git commit.
107+
commitMessage string
108+
// ghClient is used to interact with the GitHub API.
109+
ghClient GitHubClient
110+
// prType is an enum for which type of librarian pull request we are creating.
111+
prType pullRequestType
112+
// pullRequestLabels is a list of labels to add to the created pull request.
105113
pullRequestLabels []string
106-
push bool
107-
languageRepo gitrepo.Repository
108-
sourceRepo gitrepo.Repository
109-
state *config.LibrarianState
110-
workRoot string
114+
// push declares whether or not to push the commits to GitHub.
115+
push bool
116+
// languageRepo is the git repository containing the language-specific libraries.
117+
languageRepo gitrepo.Repository
118+
// sourceRepo is the git repository containing the source protos.
119+
sourceRepo gitrepo.Repository
120+
// state is the librarian state.yaml contents.
121+
state *config.LibrarianState
122+
// workRoot is the directory that we stage code changes in.
123+
workRoot string
124+
// failedGenerations is the number of generations that failed.
111125
failedGenerations int
112126
// api is the api path of a library, only set this value during api onboarding.
113127
api string
@@ -137,7 +151,9 @@ func newCommandRunner(cfg *config.Config) (*commandRunner, error) {
137151
sourceRepo gitrepo.Repository
138152
sourceRepoDir string
139153
)
140-
if cfg.CommandName == generateCmdName {
154+
155+
// If APISource is set, checkout the protos repository.
156+
if cfg.APISource != "" {
141157
sourceRepo, err = cloneOrOpenRepo(cfg.WorkRoot, cfg.APISource, cfg.APISourceDepth, defaultAPISourceBranch, cfg.CI, cfg.GitHubToken)
142158
if err != nil {
143159
return nil, err

internal/librarian/command_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1518,7 +1518,7 @@ func TestCommitAndPush(t *testing.T) {
15181518
return &mockGitHubClient{}
15191519
},
15201520
state: &config.LibrarianState{},
1521-
prType: 4,
1521+
prType: 100,
15221522
push: true,
15231523
wantErr: true,
15241524
expectedErrMsg: "failed to create pull request body",

internal/librarian/generate_command_test.go

Lines changed: 20 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func TestNewGenerateRunner(t *testing.T) {
3434
cfg *config.Config
3535
wantErr bool
3636
wantErrMsg string
37+
setupFunc func(*config.Config) error
3738
}{
3839
{
3940
name: "valid config",
@@ -102,15 +103,27 @@ func TestNewGenerateRunner(t *testing.T) {
102103
name: "clone googleapis fails",
103104
cfg: &config.Config{
104105
API: "some/api",
105-
APISource: "", // This will trigger the clone of googleapis
106+
APISource: "https://github.com/googleapis/googleapis", // This will trigger the clone of googleapis
106107
APISourceDepth: 1,
107108
Repo: newTestGitRepo(t).GetDir(),
108109
WorkRoot: t.TempDir(),
109110
Image: "gcr.io/test/test-image",
110111
CommandName: generateCmdName,
111112
},
112113
wantErr: true,
113-
wantErrMsg: "repo must be specified",
114+
wantErrMsg: "repository does not exist",
115+
setupFunc: func(cfg *config.Config) error {
116+
// The function will try to clone googleapis into the current work directory.
117+
// To make it fail, create a non-empty, non-git directory.
118+
googleapisDir := filepath.Join(cfg.WorkRoot, "googleapis")
119+
if err := os.MkdirAll(googleapisDir, 0755); err != nil {
120+
return err
121+
}
122+
if err := os.WriteFile(filepath.Join(googleapisDir, "some-file"), []byte("foo"), 0644); err != nil {
123+
return err
124+
}
125+
return nil
126+
},
114127
},
115128
{
116129
name: "valid config with local repo",
@@ -127,32 +140,11 @@ func TestNewGenerateRunner(t *testing.T) {
127140
} {
128141
t.Run(test.name, func(t *testing.T) {
129142
t.Parallel()
130-
if test.cfg.APISource == "" && test.cfg.WorkRoot != "" {
131-
if test.name == "clone googleapis fails" {
132-
// The function will try to clone googleapis into the current work directory.
133-
// To make it fail, create a non-empty, non-git directory.
134-
googleapisDir := filepath.Join(test.cfg.WorkRoot, "googleapis")
135-
if err := os.MkdirAll(googleapisDir, 0755); err != nil {
136-
t.Fatalf("os.MkdirAll() = %v", err)
137-
}
138-
if err := os.WriteFile(filepath.Join(googleapisDir, "some-file"), []byte("foo"), 0644); err != nil {
139-
t.Fatalf("os.WriteFile() = %v", err)
140-
}
141-
} else {
142-
// The function will try to clone googleapis into the current work directory.
143-
// To prevent a real clone, we can pre-create a fake googleapis repo.
144-
googleapisDir := filepath.Join(test.cfg.WorkRoot, "googleapis")
145-
if err := os.MkdirAll(googleapisDir, 0755); err != nil {
146-
t.Fatalf("os.MkdirAll() = %v", err)
147-
}
148-
runGit(t, googleapisDir, "init")
149-
runGit(t, googleapisDir, "config", "user.email", "[email protected]")
150-
runGit(t, googleapisDir, "config", "user.name", "Test User")
151-
if err := os.WriteFile(filepath.Join(googleapisDir, "README.md"), []byte("test"), 0644); err != nil {
152-
t.Fatalf("os.WriteFile: %v", err)
153-
}
154-
runGit(t, googleapisDir, "add", "README.md")
155-
runGit(t, googleapisDir, "commit", "-m", "initial commit")
143+
144+
// custom setup
145+
if test.setupFunc != nil {
146+
if err := test.setupFunc(test.cfg); err != nil {
147+
t.Fatalf("error in setup %v", err)
156148
}
157149
}
158150

internal/librarian/help.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,20 @@ Examples:
134134
135135
# Find and process all pending merged release PRs in a repository.
136136
librarian release tag-and-release --repo=https://github.com/googleapis/google-cloud-go`
137+
138+
updateImageLongHelp = `The 'update-image' command is used to update the 'image' SHA
139+
of the language container for a language repository.
140+
141+
This command's primary responsibilities are to:
142+
143+
- Update the 'image' field in '.librarian/state.yaml'
144+
- Regenerate each library with the new language container using googleapis'
145+
proto definitions at the 'last_generated_commit'
146+
147+
Examples:
148+
# Create a PR that updates the language container to latest image.
149+
librarian update-image --commit --push
150+
151+
# Create a PR that updates the language container to the specified image.
152+
librarian update-image --commit --push --image=<some-image-with-sha>`
137153
)

internal/librarian/librarian.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ func newLibrarianCommand() *cli.Command {
6969
Commands: []*cli.Command{
7070
newCmdGenerate(),
7171
cmdRelease,
72+
newCmdUpdateImage(),
7273
cmdVersion,
7374
},
7475
}
@@ -177,3 +178,39 @@ func newCmdInit() *cli.Command {
177178
addFlagVerbose(cmdInit.Flags, &verbose)
178179
return cmdInit
179180
}
181+
182+
func newCmdUpdateImage() *cli.Command {
183+
var verbose bool
184+
cmdUpdateImage := &cli.Command{
185+
Short: "update-image updates configured language image container",
186+
UsageLine: "librarian update-image [flags]",
187+
Long: updateImageLongHelp,
188+
Action: func(ctx context.Context, cmd *cli.Command) error {
189+
setupLogger(verbose)
190+
slog.Debug("update image command verbose logging")
191+
if err := cmd.Config.SetDefaults(); err != nil {
192+
return fmt.Errorf("failed to initialize config: %w", err)
193+
}
194+
if _, err := cmd.Config.IsValid(); err != nil {
195+
return fmt.Errorf("failed to validate config: %s", err)
196+
}
197+
runner, err := newUpdateImageRunner(cmd.Config)
198+
if err != nil {
199+
return err
200+
}
201+
return runner.run(ctx)
202+
},
203+
}
204+
cmdUpdateImage.Init()
205+
addFlagAPISource(cmdUpdateImage.Flags, cmdUpdateImage.Config)
206+
addFlagBuild(cmdUpdateImage.Flags, cmdUpdateImage.Config)
207+
addFlagCommit(cmdUpdateImage.Flags, cmdUpdateImage.Config)
208+
addFlagHostMount(cmdUpdateImage.Flags, cmdUpdateImage.Config)
209+
addFlagImage(cmdUpdateImage.Flags, cmdUpdateImage.Config)
210+
addFlagRepo(cmdUpdateImage.Flags, cmdUpdateImage.Config)
211+
addFlagBranch(cmdUpdateImage.Flags, cmdUpdateImage.Config)
212+
addFlagWorkRoot(cmdUpdateImage.Flags, cmdUpdateImage.Config)
213+
addFlagPush(cmdUpdateImage.Flags, cmdUpdateImage.Config)
214+
addFlagVerbose(cmdUpdateImage.Flags, &verbose)
215+
return cmdUpdateImage
216+
}

internal/librarian/mocks_test.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,13 @@ type mockContainerClient struct {
131131
failGenerateForID string
132132
// Set this value if you want an error when
133133
// generate a library with a specific id.
134-
generateErrForID error
134+
generateErrForID error
135+
// Set this value if you want an error when
136+
// build a library with a specific id.
137+
failBuildForID string
138+
// Set this value if you want an error when
139+
// build a library with a specific id.
140+
buildErrForID error
135141
requestLibraryID string
136142
noBuildResponse bool
137143
noConfigureResponse bool
@@ -164,6 +170,13 @@ func (m *mockContainerClient) Build(ctx context.Context, request *docker.BuildRe
164170
if err := os.WriteFile(filepath.Join(request.RepoDir, ".librarian", config.BuildResponse), []byte(libraryStr), 0755); err != nil {
165171
return err
166172
}
173+
174+
if m.failBuildForID != "" {
175+
if request.LibraryID == m.failBuildForID {
176+
return m.buildErrForID
177+
}
178+
}
179+
167180
return m.buildErr
168181
}
169182

@@ -308,6 +321,7 @@ type MockRepository struct {
308321
RemotesValue []*gitrepo.Remote
309322
RemotesError error
310323
CommitCalls int
324+
LastCommitMessage string
311325
GetCommitError error
312326
GetLatestCommitError error
313327
GetCommitByHash map[string]*gitrepo.Commit
@@ -330,6 +344,8 @@ type MockRepository struct {
330344
RestoreError error
331345
HeadHashValue string
332346
HeadHashError error
347+
CheckoutCalls int
348+
CheckoutError error
333349
}
334350

335351
func (m *MockRepository) HeadHash() (string, error) {
@@ -355,6 +371,7 @@ func (m *MockRepository) AddAll() error {
355371

356372
func (m *MockRepository) Commit(msg string) error {
357373
m.CommitCalls++
374+
m.LastCommitMessage = msg
358375
return m.CommitError
359376
}
360377

@@ -470,3 +487,27 @@ func (m *MockRepository) Push(name string) error {
470487
func (m *MockRepository) Restore(paths []string) error {
471488
return m.RestoreError
472489
}
490+
491+
func (m *MockRepository) CleanUntracked(paths []string) error {
492+
return nil
493+
}
494+
495+
func (m *MockRepository) Checkout(commitHash string) error {
496+
m.CheckoutCalls++
497+
if m.CheckoutError != nil {
498+
return m.CheckoutError
499+
}
500+
return nil
501+
}
502+
503+
// mockImagesClient is a mock implementation of the ImageRegistryClient interface for testing.
504+
type mockImagesClient struct {
505+
latestImage string
506+
err error
507+
findLatestCalls int
508+
}
509+
510+
func (m *mockImagesClient) FindLatest(ctx context.Context, imageName string) (string, error) {
511+
m.findLatestCalls++
512+
return m.latestImage, m.err
513+
}

0 commit comments

Comments
 (0)