func/pkg/oci/python_builder.go

194 lines
4.9 KiB
Go

package oci
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
slashpath "path"
"path/filepath"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/tarball"
)
var defaultPythonBase = "python:3.13-slim" // Moving from docker.io. See issue #2720
type pythonBuilder struct{}
func (b pythonBuilder) Base() string {
return defaultPythonBase
}
// Configure gives the python builder a chance to mutate the final
// ConfigFile that will be used when building the template.
func (b pythonBuilder) Configure(job buildJob, _ v1.Platform, cf v1.ConfigFile) (v1.ConfigFile, error) {
var (
svcRelPath, _ = filepath.Rel(job.function.Root, job.buildDir()) // eg .func/builds/by-hash/$HASH
svcPath = filepath.Join("/func", svcRelPath) // eg /func/.func/builds/by-hash/$HASH
pythonPathEnv = fmt.Sprintf("PYTHONPATH=%v/lib", svcPath)
mainPath = fmt.Sprintf("%v/service/main.py", svcPath)
listenAddrEnv = "LISTEN_ADDRESS=0.0.0.0:8080"
)
cf.Config.Env = append(cf.Config.Env, pythonPathEnv, listenAddrEnv)
cf.Config.Cmd = []string{"python", mainPath}
return cf, nil
}
func (b pythonBuilder) WriteShared(job buildJob) (layers []imageLayer, err error) {
var desc v1.Descriptor
var layer v1.Layer
// Create venv
if job.verbose {
fmt.Printf("python -m venv .venv\n")
}
cmd := exec.CommandContext(job.ctx, "python", "-m", "venv", ".venv")
cmd.Dir = job.buildDir()
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err = cmd.Run(); err != nil {
return
}
pipPath := filepath.Join(".venv", "bin", "pip")
// Upgrade pip
if job.verbose {
fmt.Printf(".venv/bin/pip install --upgrade pip\n")
}
cmd = exec.CommandContext(job.ctx, pipPath, "install", "--upgrade", "pip")
cmd.Dir = job.buildDir()
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err = cmd.Run(); err != nil {
return
}
// Install Dependencies of the current project into ./lib
// In the scaffolding direcotory.
if job.verbose {
fmt.Printf(".venv/bin/pip install . --target lib\n")
}
cmd = exec.CommandContext(job.ctx, pipPath, "install", ".", "--target", "lib")
cmd.Dir = job.buildDir()
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err = cmd.Run(); err != nil {
return
}
// Tar up the now-final build directory
source := job.buildDir()
target := filepath.Join(job.buildDir(), "lib.tar.gz")
if err = newPythonLibTarball(job, source, target); err != nil {
return
}
// Layer
if layer, err = tarball.LayerFromFile(target); err != nil {
return
}
// Descriptor
if desc, err = newDescriptor(layer); err != nil {
return
}
// Blob
blob := filepath.Join(job.blobsDir(), desc.Digest.Hex)
if job.verbose {
fmt.Printf("mv %v %v\n", rel(job.buildDir(), target), rel(job.buildDir(), blob))
}
if err = os.Rename(target, blob); err != nil {
return
}
return []imageLayer{{Descriptor: desc, Layer: layer}}, nil
}
func newPythonLibTarball(job buildJob, root, target string) error {
// Create a tarball of the "build directory"
// when extracted, it's root will be /func
// all files within should have path prefix .func/builds/by-hash/$hash
targetFile, err := os.Create(target) // final .tar.gz
if err != nil {
return err
}
defer targetFile.Close()
gw := gzip.NewWriter(targetFile)
defer gw.Close()
tw := tar.NewWriter(gw)
defer tw.Close()
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// TODO:
// This is not ideal because we have to explicitly ignore the
// oci and venv directories from being tarred and the tar from itself.
// In hindsight, it would have been better to have the "build"
// directory contain two sub-directories:
// ./dist - the scaffolding, libraries and link to the source code.
// ./container - the final OCI container.
if path == job.ociDir() {
return filepath.SkipDir
}
if path == filepath.Join(root, ".venv") {
return filepath.SkipDir
}
if path == target {
return nil
}
lnk := "" // if link, this will be used as the target
if info.Mode()&fs.ModeSymlink != 0 {
if lnk, err = validatedLinkTarget(job.function.Root, path); err != nil {
return err
}
}
header, err := tar.FileInfoHeader(info, lnk)
if err != nil {
return err
}
// The relative path from the function's root to the file
relPath, err := filepath.Rel(job.function.Root, path)
if err != nil {
return err
}
header.Name = slashpath.Join("/func/", filepath.ToSlash(relPath))
header.Uid = DefaultUid
header.Gid = DefaultGid
if err := tw.WriteHeader(header); err != nil {
return err
}
if !info.Mode().IsRegular() { //nothing more to do for non-regular
return nil
}
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(tw, file)
return err
})
}
func (b pythonBuilder) WritePlatform(ctx buildJob, p v1.Platform) (layers []imageLayer, err error) {
return []imageLayer{}, nil
}