Skip to content

Commit 230eec5

Browse files
committed
fix: remote python pack deployment
1 parent fa769ca commit 230eec5

File tree

4 files changed

+140
-7
lines changed

4 files changed

+140
-7
lines changed

cmd/func-util/main.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,12 @@ func scaffold(ctx context.Context) error {
8787
}
8888

8989
if f.Runtime != "go" && f.Runtime != "python" {
90-
// Scaffolding is for now supported/needed only for Go.
90+
// Scaffolding is for now supported/needed only for Go/Python
91+
return nil
92+
}
93+
94+
// special case for python-pack
95+
if f.Build.Builder == "pack" && f.Runtime == "python" {
9196
return nil
9297
}
9398

@@ -148,6 +153,7 @@ func deploy(ctx context.Context) error {
148153
}
149154
}
150155

156+
fmt.Printf("root: %v\n", root)
151157
f, err := fn.NewFunction(root)
152158
if err != nil {
153159
return fmt.Errorf("cannot load function: %w", err)

pkg/pipelines/tekton/pipelines_provider.go

Lines changed: 124 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,20 @@ func sourcesAsTarStream(f fn.Function) *io.PipeReader {
272272
}
273273
}
274274

275+
// Hack for python+pack where the function needs to be in a subdirectory
276+
// and the actual main (scaffolding) needs to be in the source.
277+
// So main in "root" and function in "root/subdir" otherwise pack builder
278+
// determines that current function is JAVA or something because we currently
279+
// build in the .s2i directory (needs fixing) and it contains a 'bin/' dir.
280+
// buildpacks determines its somehow java.
281+
//
282+
// You can see this in the python scaffolding injector made for local builds
283+
// for buildpacks builder & python runtime.
284+
hName := "source"
285+
usePyInjector := f.Build.Builder == "pack" && f.Runtime == "python"
286+
if usePyInjector {
287+
hName = "source/fn"
288+
}
275289
pr, pw := io.Pipe()
276290

277291
const nobodyID = 65534
@@ -282,7 +296,7 @@ func sourcesAsTarStream(f fn.Function) *io.PipeReader {
282296

283297
err := tw.WriteHeader(&tar.Header{
284298
Typeflag: tar.TypeDir,
285-
Name: "source/",
299+
Name: hName,
286300
Mode: 0777,
287301
Uid: nobodyID,
288302
Gid: nobodyID,
@@ -341,7 +355,8 @@ func sourcesAsTarStream(f fn.Function) *io.PipeReader {
341355
return fmt.Errorf("cannot create a tar header: %w", err)
342356
}
343357
// "source" is expected path in workspace pvc
344-
hdr.Name = path.Join("source", filepath.ToSlash(relp))
358+
// current Hack: python + pack builder needs subdir ('source/fn')
359+
hdr.Name = path.Join(hName, filepath.ToSlash(relp))
345360

346361
err = tw.WriteHeader(hdr)
347362
if err != nil {
@@ -365,13 +380,61 @@ func sourcesAsTarStream(f fn.Function) *io.PipeReader {
365380
if err != nil {
366381
_ = pw.CloseWithError(fmt.Errorf("error while creating tar stream from sources: %w", err))
367382
} else {
383+
// python injector hack for python+pack builder
384+
if usePyInjector {
385+
err = pythonInjector(tw, f.Invoke)
386+
if err != nil {
387+
_ = pw.CloseWithError(fmt.Errorf("cannot inject python main into tar stream: %w", err))
388+
}
389+
}
368390
_ = tw.Close()
369391
_ = pw.Close()
370392
}
371393
}()
372394
return pr
373395
}
374396

397+
// inject python main etc. into the tar file at the root dir instead of the usual
398+
// subdir for python + pack builder
399+
func pythonInjector(tw *tar.Writer, invoke string) (err error) {
400+
if invoke == "" {
401+
invoke = "http"
402+
}
403+
// the function files were all written in "source/fn" therefore new header
404+
// for the actual "source" is neeeded
405+
for _, f := range []struct {
406+
path string
407+
content string
408+
}{
409+
{
410+
path: "service/main.py",
411+
content: mainContent(invoke),
412+
},
413+
{
414+
path: "pyproject.toml",
415+
content: tomlContent,
416+
},
417+
{
418+
path: "service/__init__.py",
419+
content: "",
420+
},
421+
} {
422+
err := tw.WriteHeader(&tar.Header{
423+
Name: "/source/" + f.path,
424+
Size: int64(len(f.content)),
425+
Mode: 0644,
426+
})
427+
if err != nil {
428+
return err
429+
}
430+
_, err = tw.Write([]byte(f.content))
431+
if err != nil {
432+
return err
433+
}
434+
}
435+
return nil
436+
}
437+
375438
// Remove tries to remove all resources that are present on the cluster and belongs to the input function and it's pipelines
376439
func (pp *PipelinesProvider) Remove(ctx context.Context, f fn.Function) error {
377440
return pp.removeClusterResources(ctx, f)
@@ -574,3 +637,62 @@ func createPipelinePersistentVolumeClaim(ctx context.Context, f fn.Function, nam
574637
}
575638
return nil
576639
}
640+
641+
func mainContent(invoke string) string {
642+
template := `"""
643+
This code is glue between a user's Function and the middleware which will
644+
expose it as a network service. This code is written on-demand when a
645+
Function is being built, deployed or run. This will be included in the
646+
final container.
647+
"""
648+
import logging
649+
from func_python.%s import serve
650+
651+
logging.basicConfig(level=logging.INFO)
652+
653+
try:
654+
from function import new as handler # type: ignore[import]
655+
except ImportError:
656+
try:
657+
from function import handle as handler # type: ignore[import]
658+
except ImportError:
659+
logging.error("Function must export either 'new' or 'handle'")
660+
raise
661+
662+
def main():
663+
logging.info("Functions middleware invoking user function")
664+
serve(handler)
665+
666+
if __name__ == "__main__":
667+
main()
668+
`
669+
return fmt.Sprintf(template, invoke)
670+
}
671+
672+
const tomlContent = `[project]
673+
name = "service"
674+
description = "an autogenerated service which runs a Function"
675+
version = "0.1.0"
676+
requires-python = ">=3.9"
677+
license = "MIT"
678+
dependencies = [
679+
"func-python",
680+
"function @ ./fn"
681+
]
682+
authors = [
683+
{ name="The Knative Authors", email="[email protected]"},
684+
]
685+
686+
[build-system]
687+
requires = ["hatchling"]
688+
build-backend = "hatchling.build"
689+
690+
[tool.hatch.metadata]
691+
allow-direct-references = true
692+
693+
[tool.poetry.dependencies]
694+
python = ">=3.9,<4.0"
695+
696+
[tool.poetry.scripts]
697+
script = "service.main:main"
698+
`

pkg/pipelines/tekton/tasks.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ spec:
6363
- name: BUILDER_IMAGE
6464
description: The image on which builds will run (must include lifecycle and compatible buildpacks).
6565
- name: SOURCE_SUBPATH
66-
description: A subpath within the "source" input where the source to build is located.
67-
default: ""
66+
description: A subpath within the "source" input where the func.yaml&friends are located
67+
type: string
6868
- name: ENV_VARS
6969
type: array
7070
description: Environment variables to set during _build-time_.
@@ -160,7 +160,7 @@ spec:
160160
if [ "$(params.SOURCE_SUBPATH)" != "" ]; then
161161
func_file="$(workspaces.source.path)/$(params.SOURCE_SUBPATH)/func.yaml"
162162
fi
163-
echo "--> Saving 'func.yaml'"
163+
echo "--> Saving 'func.yaml' from '$func_file'"
164164
cp $func_file /emptyDir/func.yaml
165165
166166
############################################
@@ -183,7 +183,7 @@ spec:
183183
- name: DOCKER_CONFIG
184184
value: $(workspaces.dockerconfig.path)
185185
args:
186-
- "-app=$(workspaces.source.path)/$(params.SOURCE_SUBPATH)"
186+
- "-app=$(workspaces.source.path)"
187187
- "-cache-dir=$(workspaces.cache.path)"
188188
- "-cache-image=$(params.CACHE_IMAGE)"
189189
- "-uid=$(params.USER_ID)"

pkg/pipelines/tekton/templates.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,11 @@ func createAndApplyPipelineRunTemplate(f fn.Function, namespace string, labels m
407407
Revision: pipelinesTargetBranch,
408408
}
409409

410+
// func project is gonna be in a subdirectory
411+
if f.Runtime == "python" && f.Build.Builder == "pack" {
412+
data.ContextDir = "fn"
413+
}
414+
410415
var template string
411416
switch f.Build.Builder {
412417
case builders.Pack:

0 commit comments

Comments
 (0)