Skip to content

Commit ae87263

Browse files
authored
feat(internal/librarian): rework configure container contract (#898)
Refactor configure command container contract: Create a `generate-request.json` from state Mount `.librarian` to /librarian Mount `.librarian/generator-input` to /input Mount `apiRoot` to /source Fixes #769, fixes #815 --------- Signed-off-by: Yao Cui <[email protected]>
1 parent 3ee39a6 commit ae87263

File tree

6 files changed

+250
-40
lines changed

6 files changed

+250
-40
lines changed

internal/config/config.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ import (
2525

2626
const (
2727
// BuildRequest is a JSON file that describes which library to build/test.
28-
BuildRequest string = "build-request.json"
28+
BuildRequest string = "build-request.json"
29+
// ConfigureRequest is a JSON file that describes which library to configure.
30+
ConfigureRequest string = "configure-request.json"
2931
DefaultPushConfig string = "[email protected],Google Cloud SDK"
3032
// GeneratorInputDir is the default directory to store files that generator
3133
// needs to regenerate libraries from an empty directory.

internal/docker/docker.go

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,23 @@ type BuildRequest struct {
9999
RepoDir string
100100
}
101101

102+
// ConfigureRequest contains all the information required for a language
103+
// container to run the configure command.
104+
type ConfigureRequest struct {
105+
// cfg is a pointer to the [config.Config] struct, holding general configuration
106+
// values parsed from flags or environment variables.
107+
Cfg *config.Config
108+
// state is a pointer to the [config.LibrarianState] struct, representing
109+
// the overall state of the generation and release pipeline.
110+
State *config.LibrarianState
111+
// apiRoot specifies the root directory of the API specification repo.
112+
ApiRoot string
113+
// libraryID specifies the ID of the library to generate
114+
LibraryID string
115+
// RepoDir is the local root directory of the language repository.
116+
RepoDir string
117+
}
118+
102119
// New constructs a Docker instance which will invoke the specified
103120
// Docker image as required to implement language-specific commands,
104121
// providing the container with required environment variables.
@@ -177,22 +194,32 @@ func (c *Docker) Build(ctx context.Context, request *BuildRequest) error {
177194
}
178195

179196
// Configure configures an API within a repository, either adding it to an
180-
// existing library or creating a new library. The API is indicated by the
181-
// apiPath directory within apiRoot, and the container is provided with the
182-
// generatorInput directory to record the results of configuration. The
183-
// library code is not generated.
184-
func (c *Docker) Configure(ctx context.Context, cfg *config.Config, apiRoot, apiPath, generatorInput string) error {
197+
// existing library or creating a new library.
198+
func (c *Docker) Configure(ctx context.Context, request *ConfigureRequest) error {
199+
jsonFilePath := filepath.Join(request.RepoDir, config.LibrarianDir, config.ConfigureRequest)
200+
if err := writeRequest(request.State, request.LibraryID, jsonFilePath); err != nil {
201+
return err
202+
}
203+
defer func(name string) {
204+
err := os.Remove(name)
205+
if err != nil {
206+
slog.Warn("fail to remove file", slog.String("name", name), slog.Any("err", err))
207+
}
208+
}(jsonFilePath)
185209
commandArgs := []string{
186-
"--source=/apis",
187-
fmt.Sprintf("--%s=/%s", config.GeneratorInputDir, config.GeneratorInputDir),
188-
fmt.Sprintf("--api=%s", apiPath),
210+
"--librarian=/librarian",
211+
"--input=/input",
212+
"--source=/source",
213+
fmt.Sprintf("--library-id=%s", request.LibraryID),
189214
}
215+
generatorInput := filepath.Join(request.RepoDir, config.GeneratorInputDir)
190216
mounts := []string{
191-
fmt.Sprintf("%s:/apis", apiRoot),
192-
fmt.Sprintf("%s:/%s", generatorInput, config.GeneratorInputDir),
217+
fmt.Sprintf("%s:/librarian", config.LibrarianDir),
218+
fmt.Sprintf("%s:/input", generatorInput),
219+
fmt.Sprintf("%s:/source:ro", request.ApiRoot), // readonly volume.
193220
}
194221

195-
return c.runDocker(ctx, cfg, CommandConfigure, mounts, commandArgs)
222+
return c.runDocker(ctx, request.Cfg, CommandConfigure, mounts, commandArgs)
196223
}
197224

198225
func (c *Docker) runDocker(ctx context.Context, cfg *config.Config, command Command, mounts []string, commandArgs []string) (err error) {

internal/docker/docker_test.go

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -249,20 +249,65 @@ func TestDockerRun(t *testing.T) {
249249
Image: testImage,
250250
},
251251
runCommand: func(ctx context.Context, d *Docker) error {
252-
return d.Configure(ctx, cfg, testAPIRoot, testAPIPath, testGeneratorInput)
252+
configureRequest := &ConfigureRequest{
253+
Cfg: cfg,
254+
State: state,
255+
LibraryID: testLibraryID,
256+
RepoDir: ".",
257+
ApiRoot: testAPIRoot,
258+
}
259+
return d.Configure(ctx, configureRequest)
253260
},
254261
want: []string{
255262
"run", "--rm",
256-
"-v", fmt.Sprintf("%s:/apis", testAPIRoot),
257-
"-v", fmt.Sprintf("%s:/.librarian/generator-input", testGeneratorInput),
263+
"-v", ".librarian:/librarian",
264+
"-v", ".librarian/generator-input:/input",
265+
"-v", fmt.Sprintf("%s:/source:ro", testAPIRoot),
258266
testImage,
259267
string(CommandConfigure),
260-
"--source=/apis",
261-
"--.librarian/generator-input=/.librarian/generator-input",
262-
fmt.Sprintf("--api=%s", testAPIPath),
268+
"--librarian=/librarian",
269+
"--input=/input",
270+
"--source=/source",
271+
fmt.Sprintf("--library-id=%s", testLibraryID),
263272
},
264273
wantErr: false,
265274
},
275+
{
276+
name: "Configure with invalid repo dir",
277+
docker: &Docker{
278+
Image: testImage,
279+
},
280+
runCommand: func(ctx context.Context, d *Docker) error {
281+
configureRequest := &ConfigureRequest{
282+
Cfg: cfg,
283+
State: state,
284+
LibraryID: testLibraryID,
285+
RepoDir: "/non-exist-dir",
286+
ApiRoot: testAPIRoot,
287+
}
288+
return d.Configure(ctx, configureRequest)
289+
},
290+
want: []string{},
291+
wantErr: true,
292+
},
293+
{
294+
name: "Configure with mock image",
295+
docker: &Docker{
296+
Image: mockImage,
297+
},
298+
runCommand: func(ctx context.Context, d *Docker) error {
299+
configureRequest := &ConfigureRequest{
300+
Cfg: cfg,
301+
State: state,
302+
LibraryID: testLibraryID,
303+
RepoDir: ".",
304+
ApiRoot: testAPIRoot,
305+
}
306+
return d.Configure(ctx, configureRequest)
307+
},
308+
want: []string{},
309+
wantErr: true,
310+
},
266311
} {
267312
t.Run(test.name, func(t *testing.T) {
268313
test.docker.run = func(args ...string) error {

internal/librarian/generate.go

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,12 +163,16 @@ func (r *generateRunner) run(ctx context.Context) error {
163163
}
164164
slog.Info("Code will be generated", "dir", outputDir)
165165

166-
_, err := r.detectIfLibraryConfigured(ctx)
166+
configured, err := r.detectIfLibraryConfigured(ctx)
167167
if err != nil {
168168
return err
169169
}
170170

171-
// TODO(https://github.com/googleapis/librarian/issues/815)
171+
if !configured {
172+
if err := r.runConfigureCommand(ctx); err != nil {
173+
return err
174+
}
175+
}
172176

173177
libraryID, err := r.runGenerateCommand(ctx, outputDir)
174178
if err != nil {
@@ -416,6 +420,41 @@ func compileRegexps(patterns []string) ([]*regexp.Regexp, error) {
416420
return regexps, nil
417421
}
418422

423+
// runConfigureCommand executes the container's "configure" command for an API.
424+
//
425+
// This step prepares a language repository for a new library by adding
426+
// necessary metadata or build configurations. It finds the library's ID
427+
// from the runner's state, gathers all paths and settings, and then delegates
428+
// the execution to the container client.
429+
func (r *generateRunner) runConfigureCommand(ctx context.Context) error {
430+
apiRoot, err := filepath.Abs(r.cfg.Source)
431+
if err != nil {
432+
return err
433+
}
434+
435+
// Configuration requires a language repository to modify. If one isn't specified or
436+
// found, we cannot proceed.
437+
if r.repo == nil {
438+
slog.Error("No language repository specified; cannot run configure.", "api", r.cfg.API)
439+
return errors.New("a language repository must be specified to run configure")
440+
}
441+
442+
libraryID := findLibraryIDByAPIPath(r.state, r.cfg.API)
443+
if libraryID == "" {
444+
return errors.New("bug in Librarian: Library not found during configuration, despite being found in earlier steps")
445+
}
446+
447+
configureRequest := &docker.ConfigureRequest{
448+
Cfg: r.cfg,
449+
State: r.state,
450+
ApiRoot: apiRoot,
451+
LibraryID: libraryID,
452+
RepoDir: r.repo.Dir,
453+
}
454+
slog.Info("Performing configuration for library", "id", libraryID)
455+
return r.containerClient.Configure(ctx, configureRequest)
456+
}
457+
419458
// detectIfLibraryConfigured returns whether a library has been configured for
420459
// the requested API (as specified in apiPath). This is done by checking the local
421460
// pipeline state if repoRoot has been specified, or the remote pipeline state (just

internal/librarian/generate_test.go

Lines changed: 114 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,20 @@ import (
2424
"strings"
2525
"testing"
2626

27+
"github.com/googleapis/librarian/internal/docker"
28+
2729
"github.com/google/go-cmp/cmp"
2830
"github.com/googleapis/librarian/internal/config"
29-
"github.com/googleapis/librarian/internal/docker"
3031
"github.com/googleapis/librarian/internal/gitrepo"
3132
"gopkg.in/yaml.v3"
3233
)
3334

3435
// mockContainerClient is a mock implementation of the ContainerClient interface for testing.
3536
type mockContainerClient struct {
3637
ContainerClient
37-
generateCalls int
38-
buildCalls int
38+
generateCalls int
39+
buildCalls int
40+
configureCalls int
3941
}
4042

4143
func (m *mockContainerClient) Generate(ctx context.Context, request *docker.GenerateRequest) error {
@@ -48,6 +50,11 @@ func (m *mockContainerClient) Build(ctx context.Context, request *docker.BuildRe
4850
return nil
4951
}
5052

53+
func (m *mockContainerClient) Configure(ctx context.Context, request *docker.ConfigureRequest) error {
54+
m.configureCalls++
55+
return nil
56+
}
57+
5158
func TestDetectIfLibraryConfigured(t *testing.T) {
5259
t.Parallel()
5360
for _, test := range []struct {
@@ -287,6 +294,91 @@ func TestRunBuildCommand(t *testing.T) {
287294
}
288295
}
289296

297+
func TestRunConfigureCommand(t *testing.T) {
298+
t.Parallel()
299+
for _, test := range []struct {
300+
name string
301+
api string
302+
source string
303+
repo *gitrepo.Repository
304+
state *config.LibrarianState
305+
container *mockContainerClient
306+
wantConfigureCalls int
307+
wantErr bool
308+
}{
309+
{
310+
name: "configures library",
311+
api: "some/api",
312+
repo: newTestGitRepo(t),
313+
state: &config.LibrarianState{
314+
Libraries: []*config.LibraryState{
315+
{
316+
ID: "some-library",
317+
APIs: []*config.API{{Path: "some/api"}},
318+
},
319+
},
320+
},
321+
container: &mockContainerClient{},
322+
wantConfigureCalls: 1,
323+
},
324+
{
325+
name: "missing repo",
326+
api: "some/api",
327+
container: &mockContainerClient{},
328+
wantErr: true,
329+
},
330+
{
331+
name: "library not found in state",
332+
api: "other/api",
333+
repo: newTestGitRepo(t),
334+
state: &config.LibrarianState{
335+
Libraries: []*config.LibraryState{
336+
{
337+
ID: "some-library",
338+
APIs: []*config.API{{Path: "some/api"}},
339+
},
340+
},
341+
},
342+
container: &mockContainerClient{},
343+
wantErr: true,
344+
},
345+
{
346+
name: "error on invalid source path",
347+
source: "invalid path",
348+
repo: newTestGitRepo(t),
349+
state: &config.LibrarianState{},
350+
container: &mockContainerClient{},
351+
wantConfigureCalls: 0,
352+
wantErr: true,
353+
},
354+
} {
355+
t.Run(test.name, func(t *testing.T) {
356+
t.Parallel()
357+
sourcePath := test.source
358+
if sourcePath == "" {
359+
sourcePath = t.TempDir()
360+
}
361+
r := &generateRunner{
362+
cfg: &config.Config{
363+
API: test.api,
364+
Source: sourcePath,
365+
},
366+
repo: test.repo,
367+
state: test.state,
368+
containerClient: test.container,
369+
}
370+
371+
if err := r.runConfigureCommand(context.Background()); (err != nil) != test.wantErr {
372+
t.Errorf("runConfigureCommand() error = %v, wantErr %v", err, test.wantErr)
373+
return
374+
}
375+
if diff := cmp.Diff(test.wantConfigureCalls, test.container.configureCalls); diff != "" {
376+
t.Errorf("runConfigureCommand() configureCalls mismatch (-want +got):%s", diff)
377+
}
378+
})
379+
}
380+
}
381+
290382
func TestNewGenerateRunner(t *testing.T) {
291383
t.Parallel()
292384
for _, test := range []struct {
@@ -399,18 +491,19 @@ func runGit(t *testing.T, dir string, args ...string) {
399491
func TestGenerateRun(t *testing.T) {
400492
t.Parallel()
401493
for _, test := range []struct {
402-
name string
403-
api string
404-
repo *gitrepo.Repository
405-
state *config.LibrarianState
406-
container *mockContainerClient
407-
build bool
408-
wantErr bool
409-
wantGenerateCalls int
410-
wantBuildCalls int
494+
name string
495+
api string
496+
repo *gitrepo.Repository
497+
state *config.LibrarianState
498+
container *mockContainerClient
499+
build bool
500+
wantErr bool
501+
wantGenerateCalls int
502+
wantBuildCalls int
503+
wantConfigureCalls int
411504
}{
412505
{
413-
name: "regeneration of API",
506+
name: "generation of API",
414507
api: "some/api",
415508
repo: newTestGitRepo(t),
416509
state: &config.LibrarianState{
@@ -422,10 +515,11 @@ func TestGenerateRun(t *testing.T) {
422515
},
423516
},
424517
},
425-
container: &mockContainerClient{},
426-
build: true,
427-
wantGenerateCalls: 1,
428-
wantBuildCalls: 1,
518+
container: &mockContainerClient{},
519+
build: true,
520+
wantGenerateCalls: 1,
521+
wantBuildCalls: 1,
522+
wantConfigureCalls: 1,
429523
},
430524
{
431525
name: "symlink in output",
@@ -487,6 +581,9 @@ func TestGenerateRun(t *testing.T) {
487581
if diff := cmp.Diff(test.wantBuildCalls, test.container.buildCalls); diff != "" {
488582
t.Errorf("run() buildCalls mismatch (-want +got):%s", diff)
489583
}
584+
if diff := cmp.Diff(test.wantConfigureCalls, test.container.configureCalls); diff != "" {
585+
t.Errorf("run() configureCalls mismatch (-want +got):%s", diff)
586+
}
490587
})
491588
}
492589
}

0 commit comments

Comments
 (0)