@@ -37,11 +37,11 @@ import (
3737// plus the services.Base chain via
3838// *pythonservice.Service.
3939// FastAPI *Service — fastapi-specific state: richer Settings
40- // and the REST endpoint.
40+ // and the endpoint handles .
4141//
4242// Inherited: Init.
43- // Overridden: Load (fastapi puts source under ./code, discovers REST
44- // endpoint), Update (applies builder templates), Sync (gRPC codegen for
43+ // Overridden: Load (fastapi puts source under ./code, finds the HTTP
44+ // endpoint, if any ), Update (applies builder templates), Sync (gRPC codegen for
4545// declared dependencies and the optional service-owned gRPC server), Build
4646// (Dockerfile and build plan preparation),
4747// Deploy (k8s), Create (two-question Communicate + REST endpoint).
@@ -61,8 +61,9 @@ func NewBuilder(svc *Service) *Builder {
6161 }
6262}
6363
64- // Load overrides generic to place source under <service>/code and to
65- // discover the REST endpoint.
64+ // Load overrides generic to place source under <service>/code and to find
65+ // the endpoint the app's HTTP listener serves, which a service whose faces are
66+ // gRPC and Connect alone does not declare.
6667func (s * Builder ) Load (ctx context.Context , req * builderv0.LoadRequest ) (* builderv0.LoadResponse , error ) {
6768 defer s .Wool .Catch ()
6869
@@ -71,9 +72,8 @@ func (s *Builder) Load(ctx context.Context, req *builderv0.LoadRequest) (*builde
7172 // spec into its own half of the settings alone, leaving grpc-server and
7273 // connect-endpoint unset on every loaded service, so Sync generated no
7374 // server code and Deploy rendered no gRPC or Connect port. It reports a
74- // failure in the response's state, not the error: stop there, or the
75- // endpoint lookup below replaces the real cause with "no rest endpoint
76- // found".
75+ // failure in the response's state, not the error: stop there, so the
76+ // response carries that cause.
7777 resp , err := s .Base .Builder .LoadService (ctx , req , services.BuilderLoad {Settings : s .FastAPI .Settings })
7878 s .Service .SourceLocation = s .Service .ResolveSourceLocation ()
7979 if err != nil || resp .GetState ().GetState () == builderv0 .LoadStatus_ERROR {
@@ -96,7 +96,7 @@ func (s *Builder) Load(ctx context.Context, req *builderv0.LoadRequest) (*builde
9696 return s .Base .Builder .LoadResponse ()
9797 }
9898
99- s .FastAPI .RestEndpoint , err = resources . FindRestEndpoint (ctx , s .Endpoints )
99+ s .FastAPI .HTTPEndpoint , err = findHTTPEndpoint (ctx , s .Endpoints )
100100 if err != nil {
101101 return s .Base .Builder .LoadError (err )
102102 }
@@ -552,20 +552,27 @@ func preparePodOverlay(overlay *services.PodTemplateOverlay) error {
552552func (s * Builder ) Deploy (ctx context.Context , req * builderv0.DeploymentRequest ) (* builderv0.DeploymentResponse , error ) {
553553 defer s .Wool .Catch ()
554554
555+ // The rendered pod is probed on the app's HTTP listener, the container's
556+ // port 8080, which a service with no HTTP endpoint does not publish: say so
557+ // rather than report the missing endpoint as missing health.
558+ listener := s .FastAPI .HTTPEndpoint
559+ if listener == nil {
560+ return nil , fmt .Errorf ("the deployment probes the HTTP endpoint (api: rest or api: http), and the service declares none: declare the endpoint its probes answer on" )
561+ }
555562 // The current Core manifest profile requires all three intents. Require
556563 // an authored startup/restart policy instead of inventing one from readiness.
557- plan := resources .PlanEndpointProbes (s . FastAPI . RestEndpoint )
564+ plan := resources .PlanEndpointProbes (listener )
558565 if plan .Startup == nil || plan .Liveness == nil {
559566 return nil , fmt .Errorf ("the Kubernetes manifest profile requires explicit endpoint health.startup and health.liveness; readiness is not a restart policy" )
560567 }
561568 // A restricted render is what a cell runs, and a cell's execution
562569 // admission refuses an exec probe: probe natively there.
563- probes , err := deploymentProbesFor (s . FastAPI . RestEndpoint , req .GetDeployment ().GetKubernetes ().GetProfile ())
570+ probes , err := deploymentProbesFor (listener , req .GetDeployment ().GetKubernetes ().GetProfile ())
564571 if err != nil {
565572 return nil , err
566573 }
567574 for _ , endpoint := range s .Endpoints {
568- if endpoint .GetName () != s . FastAPI . RestEndpoint .GetName () && endpoint .GetHealth () != nil {
575+ if endpoint .GetName () != listener .GetName () && endpoint .GetHealth () != nil {
569576 return nil , fmt .Errorf ("declared health on additional endpoint %q requires a combined deployment probe; refusing to ignore it" , endpoint .GetName ())
570577 }
571578 }
@@ -665,12 +672,9 @@ func (s *Builder) Create(ctx context.Context, _ *builderv0.CreateRequest) (*buil
665672 return s .Base .Builder .CreateResponse (ctx , s .FastAPI .Settings )
666673}
667674
668- // CreateEndpoints materializes the REST endpoint.
669- //
670- // openapi/api.swagger.json is generated at Runtime time by src/openapi.py
671- // (uv run python src/openapi.py). At Create time it doesn't exist yet —
672- // that specific error is expected and swallowed with a debug log. Any
673- // other LoadRestAPI failure (malformed JSON, permission, …) propagates.
675+ // CreateEndpoints materializes the REST endpoint from the OpenAPI document
676+ // the factory templates have just written (openapi/api.swagger.json); Init
677+ // regenerates it with src/openapi.py from then on.
674678func (s * Builder ) CreateEndpoints (ctx context.Context ) error {
675679 openapiFile := s .Local ("openapi/api.swagger.json" )
676680 endpoint := s .Base .BaseEndpoint (standards .REST )
@@ -694,13 +698,9 @@ func (s *Builder) CreateEndpoints(ctx context.Context) error {
694698 endpoint .Exposure = resources .ExposurePublic
695699 }
696700
697- rest , loadErr := resources .LoadRestAPI (ctx , shared .Pointer (openapiFile ))
698- if loadErr != nil {
699- if ! errors .Is (loadErr , os .ErrNotExist ) && ! isFileNotExistErr (loadErr ) {
700- return s .Wool .Wrapf (loadErr , "cannot load rest api" )
701- }
702- s .Wool .Debug ("openapi spec not generated yet (expected at Create time)" ,
703- wool .Field ("path" , openapiFile ))
701+ rest , err := resources .LoadRestAPI (ctx , shared .Pointer (openapiFile ))
702+ if err != nil {
703+ return s .Wool .Wrapf (err , "cannot load rest api" )
704704 }
705705
706706 api , err := resources .NewAPI (ctx , endpoint , resources .ToRestAPI (rest ))
@@ -711,8 +711,8 @@ func (s *Builder) CreateEndpoints(ctx context.Context) error {
711711 if err != nil {
712712 return s .Wool .Wrapf (err , "cannot declare scaffold health" )
713713 }
714- s .FastAPI .RestEndpoint = api
715- s .Endpoints = []* basev0.Endpoint {s . FastAPI . RestEndpoint }
714+ s .FastAPI .HTTPEndpoint = api
715+ s .Endpoints = []* basev0.Endpoint {api }
716716
717717 if s .FastAPI .Settings .GRPCServer .Enabled {
718718 grpcEndpoint , grpcErr := s .grpcEndpoint (ctx )
@@ -755,13 +755,6 @@ func (s *Builder) grpcEndpoint(ctx context.Context) (*basev0.Endpoint, error) {
755755 return api , nil
756756}
757757
758- // isFileNotExistErr matches the bespoke "file does not exist" error string
759- // that resources.LoadRestAPI returns for missing files — it doesn't wrap
760- // os.ErrNotExist, so errors.Is can't catch it directly.
761- func isFileNotExistErr (err error ) bool {
762- return err != nil && err .Error () == "file does not exist"
763- }
764-
765758func (s * Builder ) populateSettingsFromAnswers () error {
766759 var err error
767760 if s .FastAPI .Settings .HotReload , err = communicate .Confirm (s .answers , HotReload ); err != nil {
0 commit comments