func/pkg/builders/buildpacks/scaffolding_injector.go

169 lines
3.6 KiB
Go

package buildpacks
import (
"archive/tar"
"context"
"errors"
"fmt"
"io"
"runtime"
"strings"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
)
// Hack implementation of DockerClient that overrides CopyToContainer method.
// The CopyToContainer method hijacks the uploaded project stream and injects function scaffolding to it.
// It basically moves content of /workspace to /workspace/fn and then setup scaffolding code directly in /workspace.
type pyScaffoldInjector struct {
client.CommonAPIClient
invoke string
}
func (s pyScaffoldInjector) CopyToContainer(ctx context.Context, ctr, p string, r io.Reader, opts container.CopyToContainerOptions) error {
if pc, _, _, ok := runtime.Caller(1); ok &&
!strings.Contains(runtime.FuncForPC(pc).Name(), "build.copyDir") {
// We are not called by "project dir copy" so we do simple direct forward call.
return s.CommonAPIClient.CopyToContainer(ctx, ctr, p, r, opts)
}
pr, pw := io.Pipe()
go func() {
var err error
defer func() {
_ = pw.CloseWithError(err)
}()
tr := tar.NewReader(r)
tw := tar.NewWriter(pw)
for {
var hdr *tar.Header
hdr, err = tr.Next()
if err != nil {
if errors.Is(err, io.EOF) {
err = nil
break
}
return
}
if strings.HasPrefix(hdr.Name, "/workspace/") {
hdr.Name = strings.Replace(hdr.Name, "/workspace/", "/workspace/fn/", 1)
}
err = tw.WriteHeader(hdr)
if err != nil {
return
}
_, err = io.Copy(tw, tr)
if err != nil {
return
}
}
err = writePythonScaffolding(tw, s.invoke)
if err != nil {
return
}
err = tw.Close()
}()
return s.CommonAPIClient.CopyToContainer(ctx, ctr, p, pr, opts)
}
func writePythonScaffolding(tw *tar.Writer, invoke string) error {
for _, f := range []struct {
path string
content string
}{
{
path: "pyproject.toml",
content: pyprojectToml,
},
{
path: "service/main.py",
content: serviceMain(invoke),
},
{
path: "service/__init__.py",
content: "",
},
} {
err := tw.WriteHeader(&tar.Header{
Name: "/workspace/" + f.path,
Size: int64(len(f.content)),
Mode: 0644,
})
if err != nil {
return err
}
_, err = tw.Write([]byte(f.content))
if err != nil {
return err
}
}
return nil
}
const pyprojectToml = `[project]
name = "service"
description = "an autogenerated service which runs a Function"
version = "0.1.0"
requires-python = ">=3.9"
license = "MIT"
dependencies = [
"func-python",
"function @ ./fn"
]
authors = [
{ name="The Knative Authors", email="knative-dev@googlegroups.com"},
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.metadata]
allow-direct-references = true
[tool.poetry.dependencies]
python = ">=3.9,<4.0"
[tool.poetry.scripts]
script = "service.main:main"
`
func serviceMain(invoke string) string {
template := `"""
This code is glue between a user's Function and the middleware which will
expose it as a network service. This code is written on-demand when a
Function is being built, deployed or run. This will be included in the
final container.
"""
import logging
from func_python.%s import serve
logging.basicConfig(level=logging.INFO)
try:
from function import new as handler # type: ignore[import]
except ImportError:
try:
from function import handle as handler # type: ignore[import]
except ImportError:
logging.error("Function must export either 'new' or 'handle'")
raise
def main():
logging.info("Functions middleware invoking user function")
serve(handler)
if __name__ == "__main__":
main()
`
return fmt.Sprintf(template, invoke)
}