Skip to content

Commit c43d3c3

Browse files
Merge pull request #112 from codefly-dev/fix/openapi-optional
fix(runtime): a service without src/openapi.py starts without one
2 parents 69267b0 + 1d04001 commit c43d3c3

2 files changed

Lines changed: 74 additions & 16 deletions

File tree

‎init_test.go‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ func initTestRuntime(t *testing.T) (*Runtime, *runtimev0.InitRequest) {
5454
require.NoError(t, os.MkdirAll(filepath.Join(source, "src"), 0o700))
5555
require.NoError(t, os.WriteFile(filepath.Join(source, "pyproject.toml"), []byte(probeProject), 0o600))
5656
require.NoError(t, os.WriteFile(filepath.Join(source, "src", "main.py"), []byte("app = None\n"), 0o600))
57+
require.NoError(t, os.WriteFile(filepath.Join(source, openAPIScript), []byte("# the scaffold's generator\n"), 0o600))
5758
runtime.Service.SourceLocation = source
5859

5960
endpoint := &basev0.Endpoint{Module: "mod", Service: "probe", Name: "rest", Api: standards.REST}
@@ -122,6 +123,41 @@ func TestInitWorksAgainstTheEnvironmentItCaptured(t *testing.T) {
122123
require.NotEmpty(t, entries, "the dependency hashes must land in the captured cache location")
123124
}
124125

126+
// TestInitWithoutAnOpenAPIGeneratorRunsNone is a service whose API is on its
127+
// gRPC and Connect faces alone: its HTTP endpoint serves the probes, and it
128+
// ships no src/openapi.py. Init must make it ready without running one, or a
129+
// fresh checkout of such a service cannot start.
130+
func TestInitWithoutAnOpenAPIGeneratorRunsNone(t *testing.T) {
131+
isolateWorkingDir(t)
132+
ctx := context.Background()
133+
runtime, req := initTestRuntime(t)
134+
require.NoError(t, os.Remove(filepath.Join(runtime.Service.SourceLocation, openAPIScript)))
135+
136+
connect := &basev0.Endpoint{Module: "mod", Service: "probe", Name: "connect", Api: standards.CONNECT}
137+
runtime.FastAPI.Settings.GRPCServer.Enabled = true
138+
runtime.FastAPI.Settings.ConnectEndpoint = true
139+
runtime.FastAPI.ConnectEndpoint = connect
140+
req.ProposedNetworkMappings = append(req.ProposedNetworkMappings, &basev0.NetworkMapping{
141+
Endpoint: connect,
142+
Instances: []*basev0.NetworkInstance{{
143+
Access: resources.NewNativeNetworkAccess(), Hostname: "localhost", Port: 9998, Address: "localhost:9998",
144+
}},
145+
})
146+
147+
env := &fakeRunnerEnvironment{}
148+
runtime.runnerEnvironment = env
149+
runtime.cacheLocation = t.TempDir()
150+
151+
resp, err := runtime.Init(ctx, req)
152+
require.NoError(t, err)
153+
require.Equal(t, runtimev0.InitStatus_READY, resp.GetStatus().GetState(), resp.GetStatus().GetMessage())
154+
var commands [][]string
155+
for _, proc := range env.started() {
156+
commands = append(commands, append([]string{proc.bin}, proc.args...))
157+
}
158+
require.Equal(t, [][]string{{"uv", "sync"}}, commands, "no OpenAPI generator, so none runs")
159+
}
160+
125161
// TestInitIsSafeWithConcurrentTeardown overlaps Init with the calls that own
126162
// the same fields. Under `-race` this is what catches Init publishing or
127163
// reading the runner state outside runnerMu; without it, it still pins that

‎runtime.go‎

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -568,25 +568,47 @@ func (s *Runtime) Init(ctx context.Context, req *runtimev0.InitRequest) (*runtim
568568
}
569569
s.Wool.Debug("successful init of runner")
570570

571+
if err := s.initOpenAPI(ctx, env, cacheLocation); err != nil {
572+
return s.Base.Runtime.InitErrorf(err, "cannot generate Open API document")
573+
}
574+
return s.Base.Runtime.InitResponse()
575+
}
576+
577+
// openAPIScript is the project's OpenAPI generator, relative to its source
578+
// location: the scaffold ships it, and it dumps the schema of the app's REST
579+
// routes to openapi/api.swagger.json.
580+
const openAPIScript = "src/openapi.py"
581+
582+
// initOpenAPI regenerates the OpenAPI document when the project ships the
583+
// generator and src/main.py changed since the last run. A project without the
584+
// generator publishes no OpenAPI document: its HTTP endpoint serves the probes
585+
// and its API is on another face (gRPC, Connect), so there is nothing to
586+
// generate and nothing is run.
587+
func (s *Runtime) initOpenAPI(ctx context.Context, env runners.RunnerEnvironment, cacheLocation string) error {
588+
script := path.Join(s.Service.SourceLocation, openAPIScript)
589+
hasScript, err := shared.FileExists(ctx, script)
590+
if err != nil {
591+
return err
592+
}
593+
if !hasScript {
594+
s.Wool.Debug("no OpenAPI generator: the HTTP endpoint publishes no OpenAPI document", wool.Field("path", script))
595+
return nil
596+
}
571597
openAPI := builders.NewDependencies("api",
572598
builders.NewDependency(path.Join(s.Service.SourceLocation, "src/main.py"))).WithCache(cacheLocation)
573-
openApiUpdate, err := openAPI.Updated(ctx)
574-
if err != nil {
575-
return s.Base.Runtime.InitError(err)
599+
openAPIUpdate, err := openAPI.Updated(ctx)
600+
if err != nil || !openAPIUpdate {
601+
return err
576602
}
577-
578-
if openApiUpdate {
579-
s.Infof("generating Open API document")
580-
if err := s.GenerateOpenAPI(ctx, env); err != nil {
581-
return s.Base.Runtime.InitErrorf(err, "cannot generate Open API document")
582-
}
583-
if err := openAPI.UpdateCache(ctx); err != nil {
584-
s.Wool.Warn("cannot update openapi cache", wool.ErrField(err))
585-
}
586-
s.Wool.Debug("generate Open API done")
603+
s.Infof("generating Open API document")
604+
if err := s.GenerateOpenAPI(ctx, env); err != nil {
605+
return err
587606
}
588-
589-
return s.Base.Runtime.InitResponse()
607+
if err := openAPI.UpdateCache(ctx); err != nil {
608+
s.Wool.Warn("cannot update openapi cache", wool.ErrField(err))
609+
}
610+
s.Wool.Debug("generate Open API done")
611+
return nil
590612
}
591613

592614
func (s *Runtime) Start(ctx context.Context, req *runtimev0.StartRequest) (*runtimev0.StartResponse, error) {
@@ -1358,7 +1380,7 @@ func (s *Runtime) EventHandler(event code.Change) error {
13581380
// the OpenAPI spec. Convention: the project ships a small openapi.py that
13591381
// imports src.main and dumps the schema. See templates/factory.
13601382
func (s *Runtime) GenerateOpenAPI(ctx context.Context, env runners.RunnerEnvironment) error {
1361-
proc, err := env.NewProcess("uv", "run", "python", "src/openapi.py")
1383+
proc, err := env.NewProcess("uv", "run", "python", openAPIScript)
13621384
if err != nil {
13631385
return s.Wool.Wrapf(err, "cannot create openapi runner")
13641386
}

0 commit comments

Comments
 (0)