Skip to content

Commit 879233d

Browse files
authored
fix: Python local buildpack build (knative#2907)
* fix: Python local buildpack build Signed-off-by: Matej Vašek <[email protected]> * fix: sane default for LISTEN_ADDRESS in pack build Signed-off-by: Matej Vašek <[email protected]> --------- Signed-off-by: Matej Vašek <[email protected]>
1 parent 18a119a commit 879233d

File tree

2 files changed

+171
-4
lines changed

2 files changed

+171
-4
lines changed

pkg/builders/buildpacks/builder.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,6 @@ var DefaultLifecycleImage = "docker.io/buildpacksio/lifecycle:553c041"
120120

121121
// Build the Function at path.
122122
func (b *Builder) Build(ctx context.Context, f fn.Function, platforms []fn.Platform) (err error) {
123-
if f.Runtime == "python" {
124-
return fmt.Errorf("python is not currently supported with pack builder (use host or s2i builder instead")
125-
}
126-
127123
if len(platforms) != 0 {
128124
return errors.New("the pack builder does not support specifying target platforms directly")
129125
}
@@ -186,6 +182,10 @@ func (b *Builder) Build(ctx context.Context, f fn.Function, platforms []fn.Platf
186182
opts.ContainerConfig.Network = "host"
187183
}
188184

185+
if _, ok := opts.Env["BPE_DEFAULT_LISTEN_ADDRESS"]; !ok {
186+
opts.Env["BPE_DEFAULT_LISTEN_ADDRESS"] = "[::]:8080"
187+
}
188+
189189
var bindings = make([]string, 0, len(f.Build.Mounts))
190190
for _, m := range f.Build.Mounts {
191191
bindings = append(bindings, fmt.Sprintf("%s:%s", m.Source, m.Destination))
@@ -215,6 +215,10 @@ func (b *Builder) Build(ctx context.Context, f fn.Function, platforms []fn.Platf
215215
return fmt.Errorf("podman 4.3 is not supported, use podman 4.2 or 4.4")
216216
}
217217

218+
if f.Runtime == "python" {
219+
cli = pyScaffoldInjector{cli}
220+
}
221+
218222
// Client with a logger which is enabled if in Verbose mode and a dockerClient that supports SSH docker daemon connection.
219223
if impl, err = pack.NewClient(pack.WithLogger(b.logger), pack.WithDockerClient(cli)); err != nil {
220224
return fmt.Errorf("cannot create pack client: %w", err)
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package buildpacks
2+
3+
import (
4+
"archive/tar"
5+
"context"
6+
"errors"
7+
"io"
8+
"runtime"
9+
"strings"
10+
11+
"github.com/docker/docker/api/types/container"
12+
"github.com/docker/docker/client"
13+
)
14+
15+
// Hack implementation of DockerClient that overrides CopyToContainer method.
16+
// The CopyToContainer method hijacks the uploaded project stream and injects function scaffolding to it.
17+
// It basically moves content of /workspace to /workspace/fn and then setup scaffolding code directly in /workspace.
18+
type pyScaffoldInjector struct {
19+
client.CommonAPIClient
20+
}
21+
22+
func (s pyScaffoldInjector) CopyToContainer(ctx context.Context, ctr, p string, r io.Reader, opts container.CopyToContainerOptions) error {
23+
24+
if pc, _, _, ok := runtime.Caller(1); ok &&
25+
!strings.Contains(runtime.FuncForPC(pc).Name(), "build.copyDir") {
26+
// We are not called by "project dir copy" so we do simple direct forward call.
27+
return s.CommonAPIClient.CopyToContainer(ctx, ctr, p, r, opts)
28+
}
29+
30+
pr, pw := io.Pipe()
31+
go func() {
32+
var err error
33+
defer func() {
34+
_ = pw.CloseWithError(err)
35+
}()
36+
tr := tar.NewReader(r)
37+
tw := tar.NewWriter(pw)
38+
39+
for {
40+
var hdr *tar.Header
41+
hdr, err = tr.Next()
42+
43+
if err != nil {
44+
if errors.Is(err, io.EOF) {
45+
err = nil
46+
break
47+
}
48+
return
49+
}
50+
51+
if strings.HasPrefix(hdr.Name, "/workspace/") {
52+
hdr.Name = strings.Replace(hdr.Name, "/workspace/", "/workspace/fn/", 1)
53+
}
54+
55+
err = tw.WriteHeader(hdr)
56+
if err != nil {
57+
return
58+
}
59+
_, err = io.Copy(tw, tr)
60+
if err != nil {
61+
return
62+
}
63+
}
64+
err = writePythonScaffolding(tw)
65+
if err != nil {
66+
return
67+
}
68+
err = tw.Close()
69+
}()
70+
71+
return s.CommonAPIClient.CopyToContainer(ctx, ctr, p, pr, opts)
72+
}
73+
74+
func writePythonScaffolding(tw *tar.Writer) error {
75+
for _, f := range []struct {
76+
path string
77+
content string
78+
}{
79+
{
80+
path: "pyproject.toml",
81+
content: pyprojectToml,
82+
},
83+
{
84+
path: "service/main.py",
85+
content: serviceMain,
86+
},
87+
{
88+
path: "service/__init__.py",
89+
content: "",
90+
},
91+
} {
92+
err := tw.WriteHeader(&tar.Header{
93+
Name: "/workspace/" + f.path,
94+
Size: int64(len(f.content)),
95+
Mode: 0644,
96+
})
97+
if err != nil {
98+
return err
99+
}
100+
_, err = tw.Write([]byte(f.content))
101+
if err != nil {
102+
return err
103+
}
104+
}
105+
return nil
106+
}
107+
108+
const pyprojectToml = `[project]
109+
name = "service"
110+
description = "an autogenerated service which runs a Function"
111+
version = "0.1.0"
112+
requires-python = ">=3.9"
113+
license = "MIT"
114+
dependencies = [
115+
"func-python",
116+
"function @ ./fn"
117+
]
118+
authors = [
119+
{ name="The Knative Authors", email="[email protected]"},
120+
]
121+
122+
[build-system]
123+
requires = ["hatchling"]
124+
build-backend = "hatchling.build"
125+
126+
[tool.hatch.metadata]
127+
allow-direct-references = true
128+
129+
[tool.poetry.dependencies]
130+
python = ">=3.9,<4.0"
131+
132+
[tool.poetry.scripts]
133+
script = "service.main:main"
134+
`
135+
136+
const serviceMain = `"""
137+
This code is glue between a user's Function and the middleware which will
138+
expose it as a network service. This code is written on-demand when a
139+
Function is being built, deployed or run. This will be included in the
140+
final container.
141+
"""
142+
import logging
143+
from func_python.cloudevent import serve
144+
145+
logging.basicConfig(level=logging.INFO)
146+
147+
try:
148+
from function import new as handler # type: ignore[import]
149+
except ImportError:
150+
try:
151+
from function import handle as handler # type: ignore[import]
152+
except ImportError:
153+
logging.error("Function must export either 'new' or 'handle'")
154+
raise
155+
156+
157+
def main():
158+
logging.info("Functions middleware invoking user function")
159+
serve(handler)
160+
161+
if __name__ == "__main__":
162+
main()
163+
`

0 commit comments

Comments
 (0)