diff --git a/cmd/run.go b/cmd/run.go index 5533a76e39..9d5b10d135 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -27,8 +27,8 @@ NAME {{rootCmdUse}} run - Run a function locally SYNOPSIS - {{rootCmdUse}} run [-t|--container] [-r|--registry] [-i|--image] [-e|--env] - [--build] [-b|--builder] [--builder-image] [-c|--confirm] + {{rootCmdUse}} run [-r|--registry] [-i|--image] [-e|--env] [--build] + [-b|--builder] [--builder-image] [-c|--confirm] [--address] [--json] [-v|--verbose] DESCRIPTION @@ -37,26 +37,19 @@ DESCRIPTION Values provided for flags are not persisted to the function's metadata. Containerized Runs - The --container flag indicates that the function's container should be - run rather than running the source code directly. This may require that - the function's container first be rebuilt. Building the container on or - off can be altered using the --build flag. The value --build=auto - can be used to indicate the function should be run in a container, with - the container automatically built if necessary. - - The --container flag defaults to true if the builder defined for the - function is a containerized builder such as Pack or S2I, and in the case - where the function's runtime requires containerized builds (is not yet - supported by the Host builder. + You can build your function in a container using the Pack or S2i builders. + On the contrary, non-containerized run is achieved via Host builder which + will use your host OS' environment to build the function. This builder is + currently enabled for Go and Python. Building defaults to using the Host + builder when available. You can alter this by using the --builder flag + eg: --builder=s2i. Process Scaffolding - This is an Experimental Feature currently available only to Go projects. - When running a function with --container=false (host-based runs), the - function is first wrapped code which presents it as a process. - This "scaffolding" is transient, written for each build or run, and should - in most cases be transparent to a function author. However, to customize, - or even completely replace this scafolding code, see the 'scaffold' - subcommand. + This is an Experimental Feature currently available only to Go and Python + projects. When running a function with --builder=host, the function is + first wrapped with code which presents it as a process. This "scaffolding" + is transient, written for each build or run, and should in most cases be + transparent to a function author. EXAMPLES @@ -64,11 +57,12 @@ EXAMPLES $ {{rootCmdUse}} run o Run the function locally from within its container, forcing a rebuild - of the container even if no filesysem changes are detected - $ {{rootCmdUse}} run --build + of the container even if no filesystem changes are detected. There are 2 + builders available for containerized build - 'pack' and 's2i'. + $ {{rootCmdUse}} run --build= - o Run the function locally on the host with no containerization (Go only). - $ {{rootCmdUse}} run --container=false + o Run the function locally on the host with no containerization (Go/Python only). + $ {{rootCmdUse}} run --builder=host o Run the function locally on a specific address. $ {{rootCmdUse}} run --address='[::]:8081' @@ -78,7 +72,7 @@ EXAMPLES `, SuggestFor: []string{"rnu"}, PreRunE: bindEnv("build", "builder", "builder-image", "base-image", - "confirm", "container", "env", "image", "path", "registry", + "confirm", "env", "image", "path", "registry", "start-timeout", "verbose", "address", "json"), RunE: func(cmd *cobra.Command, _ []string) error { return runRun(cmd, newClient) @@ -120,8 +114,6 @@ EXAMPLES "You may provide this flag multiple times for setting multiple environment variables. "+ "To unset, specify the environment variable name followed by a \"-\" (e.g., NAME-).") cmd.Flags().Duration("start-timeout", f.Run.StartTimeout, fmt.Sprintf("time this function needs in order to start. If not provided, the client default %v will be in effect. ($FUNC_START_TIMEOUT)", fn.DefaultStartTimeout)) - cmd.Flags().BoolP("container", "t", runContainerizedByDefault(f), - "Run the function in a container. ($FUNC_CONTAINER)") // TODO: Without the "Host" builder enabled, this code-path is unreachable, // so remove hidden flag when either the Host builder path is available, @@ -156,10 +148,6 @@ EXAMPLES return cmd } -func runContainerizedByDefault(f fn.Function) bool { - return f.Build.Builder == "pack" || f.Build.Builder == "s2i" || !oci.IsSupported(f.Runtime) -} - func runRun(cmd *cobra.Command, newClient ClientFactory) (err error) { var ( cfg runConfig @@ -170,16 +158,20 @@ func runRun(cmd *cobra.Command, newClient ClientFactory) (err error) { if f, err = fn.NewFunction(cfg.Path); err != nil { return } - if err = cfg.Validate(cmd, f); err != nil { - return - } if !f.Initialized() { return fn.NewErrNotInitialized(f.Root) } + + if err = cfg.Validate(cmd, f); err != nil { + return + } + if f, err = cfg.Configure(f); err != nil { // Updates f with deploy cfg return } + container := f.Build.Builder != "host" + // Ignore the verbose flag if JSON output if cfg.JSON { cfg.Verbose = false @@ -190,7 +182,7 @@ func runRun(cmd *cobra.Command, newClient ClientFactory) (err error) { if err != nil { return } - if cfg.Container { + if container { clientOptions = append(clientOptions, fn.WithRunner(docker.NewRunner(cfg.Verbose, os.Stdout, os.Stderr))) } if cfg.StartTimeout != 0 { @@ -204,7 +196,7 @@ func runRun(cmd *cobra.Command, newClient ClientFactory) (err error) { // // If requesting to run via the container, build the container if it is // either out-of-date or a build was explicitly requested. - if cfg.Container { + if container { var digested bool buildOptions, err := cfg.buildOptions() @@ -218,23 +210,17 @@ func runRun(cmd *cobra.Command, newClient ClientFactory) (err error) { if err != nil { return err } - if !digested { - // assign valid undigested image - f.Build.Image = cfg.Image - } - } - - if digested { - // run cmd takes f.Build.Image - see newContainerConfig in docker/runner.go - // it doesnt get saved, just runtime image + // image was parsed and both digested AND undigested imgs are valid f.Build.Image = cfg.Image - } else { + } + // actual build step + if !digested { if f, _, err = build(cmd, cfg.Build, f, client, buildOptions); err != nil { return err } } - } else { + } else { // if !container // dont run digested image without a container if cfg.Image != "" { digested, err := isDigested(cfg.Image) @@ -242,7 +228,7 @@ func runRun(cmd *cobra.Command, newClient ClientFactory) (err error) { return err } if digested { - return fmt.Errorf("cannot use digested image with --container=false") + return fmt.Errorf("cannot use digested image with non-containerized builds (--builder=host)") } } } @@ -314,10 +300,6 @@ type runConfig struct { // Can be 'auto' or a truthy value. Build string - // Container indicates the function should be run in a container. - // Requires the container be built. - Container bool - // Env variables. may include removals using a "-" Env []string @@ -337,7 +319,6 @@ func newRunConfig(cmd *cobra.Command) (c runConfig) { buildConfig: newBuildConfig(), Build: viper.GetString("build"), Env: viper.GetStringSlice("env"), - Container: viper.GetBool("container"), StartTimeout: viper.GetDuration("start-timeout"), Address: viper.GetString("address"), JSON: viper.GetBool("json"), @@ -363,7 +344,7 @@ func (c runConfig) Configure(f fn.Function) (fn.Function, error) { f.Run.Envs, err = applyEnvs(f.Run.Envs, c.Env) - // The other members; build, path, and container; are not part of function + // The other members; build and path; are not part of function // state, so are not mentioned here in Configure. return f, err } @@ -396,13 +377,13 @@ func (c runConfig) Validate(cmd *cobra.Command, f fn.Function) (err error) { } } - if !c.Container && !oci.IsSupported(f.Runtime) { + if f.Build.Builder == "host" && !oci.IsSupported(f.Runtime) { return fmt.Errorf("the %q runtime currently requires being run in a container", f.Runtime) } // When the docker runner respects the StartTimeout, this validation check // can be removed - if c.StartTimeout != 0 && c.Container { + if c.StartTimeout != 0 && f.Build.Builder != "host" { return errors.New("the ability to specify the startup timeout for containerized runs is coming soon") } diff --git a/docs/reference/func_run.md b/docs/reference/func_run.md index 2f9a7cdb37..20092f91dc 100644 --- a/docs/reference/func_run.md +++ b/docs/reference/func_run.md @@ -9,8 +9,8 @@ NAME func run - Run a function locally SYNOPSIS - func run [-t|--container] [-r|--registry] [-i|--image] [-e|--env] - [--build] [-b|--builder] [--builder-image] [-c|--confirm] + func run [-r|--registry] [-i|--image] [-e|--env] [--build] + [-b|--builder] [--builder-image] [-c|--confirm] [--address] [--json] [-v|--verbose] DESCRIPTION @@ -19,26 +19,19 @@ DESCRIPTION Values provided for flags are not persisted to the function's metadata. Containerized Runs - The --container flag indicates that the function's container should be - run rather than running the source code directly. This may require that - the function's container first be rebuilt. Building the container on or - off can be altered using the --build flag. The value --build=auto - can be used to indicate the function should be run in a container, with - the container automatically built if necessary. - - The --container flag defaults to true if the builder defined for the - function is a containerized builder such as Pack or S2I, and in the case - where the function's runtime requires containerized builds (is not yet - supported by the Host builder. + You can build your function in a container using the Pack or S2i builders. + On the contrary, non-containerized run is achieved via Host builder which + will use your host OS' environment to build the function. This builder is + currently enabled for Go and Python. Building defaults to using the Host + builder when available. You can alter this by using the --builder flag + eg: --builder=s2i. Process Scaffolding - This is an Experimental Feature currently available only to Go projects. - When running a function with --container=false (host-based runs), the - function is first wrapped code which presents it as a process. - This "scaffolding" is transient, written for each build or run, and should - in most cases be transparent to a function author. However, to customize, - or even completely replace this scafolding code, see the 'scaffold' - subcommand. + This is an Experimental Feature currently available only to Go and Python + projects. When running a function with --builder=host, the function is + first wrapped with code which presents it as a process. This "scaffolding" + is transient, written for each build or run, and should in most cases be + transparent to a function author. EXAMPLES @@ -46,11 +39,12 @@ EXAMPLES $ func run o Run the function locally from within its container, forcing a rebuild - of the container even if no filesysem changes are detected - $ func run --build + of the container even if no filesystem changes are detected. There are 2 + builders available for containerized build - 'pack' and 's2i'. + $ func run --build= - o Run the function locally on the host with no containerization (Go only). - $ func run --container=false + o Run the function locally on the host with no containerization (Go/Python only). + $ func run --builder=host o Run the function locally on a specific address. $ func run --address='[::]:8081' @@ -72,7 +66,6 @@ func run -b, --builder string Builder to use when creating the function's container. Currently supported builders are "host", "pack" and "s2i". (default "pack") --builder-image string Specify a custom builder image for use by the builder other than its default. ($FUNC_BUILDER_IMAGE) -c, --confirm Prompt to confirm options interactively ($FUNC_CONFIRM) - -t, --container Run the function in a container. ($FUNC_CONTAINER) (default true) -e, --env stringArray Environment variable to set in the form NAME=VALUE. You may provide this flag multiple times for setting multiple environment variables. To unset, specify the environment variable name followed by a "-" (e.g., NAME-). -h, --help help for run -i, --image string Full image name in the form [registry]/[namespace]/[name]:[tag]. This option takes precedence over --registry. Specifying tag is optional. ($FUNC_IMAGE) diff --git a/hack/allocate.sh b/hack/allocate.sh index c2dcae3ac1..bfc4dd40e9 100755 --- a/hack/allocate.sh +++ b/hack/allocate.sh @@ -351,9 +351,54 @@ dapr_runtime() { # to only start a single instance rather than four. # helm repo add bitnami https://charts.bitnami.com/bitnami echo "${blue}- Redis ${reset}" - $HELM repo add bitnami https://charts.bitnami.com/bitnami - $HELM install redis bitnami/redis --set image.tag=6.2 - $HELM repo update + # Deploy Redis using simple manifest with official Redis image + # (Bitnami images have migration issues as of Sept 2025) + $KUBECTL apply -f - << EOF +apiVersion: v1 +kind: Service +metadata: + name: redis-master + namespace: default +spec: + ports: + - port: 6379 + targetPort: 6379 + selector: + app: redis +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: redis-master + namespace: default +spec: + serviceName: redis-master + replicas: 1 + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: redis:7-alpine + ports: + - containerPort: 6379 + volumeMounts: + - name: redis-storage + mountPath: /data + volumeClaimTemplates: + - metadata: + name: redis-storage + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 1Gi +EOF # 2) Expose a Redis-backed Dapr State Storage component echo "${blue}- State Storage Component${reset}" @@ -370,9 +415,7 @@ spec: - name: redisHost value: redis-master.default.svc.cluster.local:6379 - name: redisPassword - secretKeyRef: - name: redis - key: redis-password + value: "" EOF # 3) Expose A Redis-backed Dapr Pub/Sub Component @@ -390,9 +433,7 @@ spec: - name: redisHost value: redis-master.default.svc.cluster.local:6379 - name: redisPassword - secretKeyRef: - name: redis - key: redis-password + value: "" EOF echo "${green}✅ Dapr Runtime${reset}" diff --git a/hack/install-binaries.sh b/hack/install-binaries.sh index 1395f1c829..dc7f328d58 100755 --- a/hack/install-binaries.sh +++ b/hack/install-binaries.sh @@ -28,7 +28,7 @@ install_binaries() { local kubectl_version=1.33.1 local kind_version=0.29.0 - local dapr_version=1.14.1 + local dapr_version=1.16.0 local helm_version=3.18.0 local stern_version=1.32.0 local kn_version=1.18.0 diff --git a/pkg/builders/buildpacks/builder.go b/pkg/builders/buildpacks/builder.go index 0954b603b9..e703f18de8 100644 --- a/pkg/builders/buildpacks/builder.go +++ b/pkg/builders/buildpacks/builder.go @@ -216,7 +216,16 @@ func (b *Builder) Build(ctx context.Context, f fn.Function, platforms []fn.Platf } if f.Runtime == "python" { - cli = pyScaffoldInjector{cli} + if fi, _ := os.Lstat(filepath.Join(f.Root, "Procfile")); fi == nil { + // HACK (of a hack): need to get the right invocation signature + // the standard scaffolding does this in toSignature() func. + // we know we have python here. + invoke := f.Invoke + if invoke == "" { + invoke = "http" + } + cli = pyScaffoldInjector{cli, invoke} + } } // Client with a logger which is enabled if in Verbose mode and a dockerClient that supports SSH docker daemon connection. diff --git a/pkg/builders/buildpacks/scaffolding_injector.go b/pkg/builders/buildpacks/scaffolding_injector.go index 9b380f2471..1ccd17e4cf 100644 --- a/pkg/builders/buildpacks/scaffolding_injector.go +++ b/pkg/builders/buildpacks/scaffolding_injector.go @@ -4,6 +4,7 @@ import ( "archive/tar" "context" "errors" + "fmt" "io" "runtime" "strings" @@ -17,6 +18,7 @@ import ( // 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 { @@ -61,7 +63,7 @@ func (s pyScaffoldInjector) CopyToContainer(ctx context.Context, ctr, p string, return } } - err = writePythonScaffolding(tw) + err = writePythonScaffolding(tw, s.invoke) if err != nil { return } @@ -71,7 +73,7 @@ func (s pyScaffoldInjector) CopyToContainer(ctx context.Context, ctr, p string, return s.CommonAPIClient.CopyToContainer(ctx, ctr, p, pr, opts) } -func writePythonScaffolding(tw *tar.Writer) error { +func writePythonScaffolding(tw *tar.Writer, invoke string) error { for _, f := range []struct { path string content string @@ -82,7 +84,7 @@ func writePythonScaffolding(tw *tar.Writer) error { }, { path: "service/main.py", - content: serviceMain, + content: serviceMain(invoke), }, { path: "service/__init__.py", @@ -128,19 +130,21 @@ allow-direct-references = true [tool.poetry.dependencies] python = ">=3.9,<4.0" +function = { path = "fn", develop = true } [tool.poetry.scripts] script = "service.main:main" ` -const serviceMain = `""" +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.cloudevent import serve +from func_python.%s import serve logging.basicConfig(level=logging.INFO) @@ -161,3 +165,5 @@ def main(): if __name__ == "__main__": main() ` + return fmt.Sprintf(template, invoke) +} diff --git a/pkg/functions/runner.go b/pkg/functions/runner.go index f0dedf2bdb..aa51cdb384 100644 --- a/pkg/functions/runner.go +++ b/pkg/functions/runner.go @@ -195,7 +195,7 @@ func runPython(ctx context.Context, job *Job) (err error) { if job.verbose { fmt.Printf("python -m venv .venv\n") } - cmd := exec.CommandContext(ctx, "python", "-m", "venv", ".venv") + cmd := exec.CommandContext(ctx, pythonCmd(), "-m", "venv", ".venv") cmd.Dir = job.Dir() cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout @@ -360,3 +360,11 @@ func choosePort(iface, preferredPort string) (string, error) { } return port, nil } + +func pythonCmd() string { + _, err := exec.LookPath("python") + if err != nil { + return "python3" + } + return "python" +} diff --git a/pkg/oci/python_builder.go b/pkg/oci/python_builder.go index 3cc3126559..4272590928 100644 --- a/pkg/oci/python_builder.go +++ b/pkg/oci/python_builder.go @@ -10,6 +10,7 @@ import ( "os/exec" slashpath "path" "path/filepath" + "regexp" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/tarball" @@ -23,7 +24,17 @@ func (b pythonBuilder) Base(customBase string) string { if customBase != "" { return customBase } - return defaultPythonBase + cmd := exec.Command(pythonCmd(), "-V") + out, err := cmd.CombinedOutput() + if err != nil { + return defaultPythonBase + } + re := regexp.MustCompile(`Python (\d+\.\d+)\.\d+`) + subMatches := re.FindSubmatch(out) + if len(subMatches) != 2 { + return defaultPythonBase + } + return fmt.Sprintf("python:%s-slim", subMatches[1]) } // Configure gives the python builder a chance to mutate the final @@ -50,7 +61,7 @@ func (b pythonBuilder) WriteShared(job buildJob) (layers []imageLayer, err error if job.verbose { fmt.Printf("python -m venv .venv\n") } - cmd := exec.CommandContext(job.ctx, "python", "-m", "venv", ".venv") + cmd := exec.CommandContext(job.ctx, pythonCmd(), "-m", "venv", ".venv") cmd.Dir = job.buildDir() cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout @@ -194,3 +205,11 @@ func newPythonLibTarball(job buildJob, root, target string) error { func (b pythonBuilder) WritePlatform(ctx buildJob, p v1.Platform) (layers []imageLayer, err error) { return []imageLayer{}, nil } + +func pythonCmd() string { + _, err := exec.LookPath("python") + if err != nil { + return "python3" + } + return "python" +} diff --git a/pkg/pipelines/tekton/gitlab_int_test.go b/pkg/pipelines/tekton/gitlab_int_test.go index 5da6929036..ba2639e946 100644 --- a/pkg/pipelines/tekton/gitlab_int_test.go +++ b/pkg/pipelines/tekton/gitlab_int_test.go @@ -503,7 +503,7 @@ func getAPIToken(baseURL, username, password string) (string, error) { } data := struct { - NewToken string `json:"new_token,omitempty"` + NewToken string `json:"token,omitempty"` }{} e := json.NewDecoder(resp.Body) err = e.Decode(&data) diff --git a/pkg/ssh/ssh_dialer_test.go b/pkg/ssh/ssh_dialer_test.go index 1369e89ce3..828a0166fc 100644 --- a/pkg/ssh/ssh_dialer_test.go +++ b/pkg/ssh/ssh_dialer_test.go @@ -44,6 +44,7 @@ type testParams struct { setUpEnv setUpEnvFn skipOnWin bool skipOnRoot bool + requireIPv6 bool CreateError string DialError string } @@ -192,7 +193,8 @@ func TestCreateDialer(t *testing.T) { connConfig.hostIPv6, connConfig.portIPv6, )}, - setUpEnv: all(withoutSSHAgent, withCleanHome, withKnowHosts(connConfig)), + requireIPv6: true, + setUpEnv: all(withoutSSHAgent, withCleanHome, withKnowHosts(connConfig)), }, { name: "broken known host", @@ -361,13 +363,13 @@ func TestCreateDialer(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - u, err := url.Parse(tt.args.connStr) - th.AssertNil(t, err) - - if net.ParseIP(u.Hostname()).To4() == nil && connConfig.hostIPv6 == "" { + if tt.requireIPv6 && connConfig.hostIPv6 == "" { t.Skip("skipping ipv6 test since test environment doesn't support ipv6 connection") } + u, err := url.Parse(tt.args.connStr) + th.AssertNil(t, err) + if tt.skipOnWin && runtime.GOOS == "windows" { t.Skip("skipping this test on windows") } diff --git a/test/e2e/scenario_no_container_test.go b/test/e2e/scenario_no_container_test.go index e69145bf2e..36462e79e0 100644 --- a/test/e2e/scenario_no_container_test.go +++ b/test/e2e/scenario_no_container_test.go @@ -21,7 +21,7 @@ import ( ) // TestFunctionRunWithoutContainer tests the func runs on host without container (golang funcs only) -// In other words, it tests `func run --container=false` +// In other words, it tests `func run --builder=host` func TestFunctionRunWithoutContainer(t *testing.T) { var funcName = "func-no-container" @@ -56,7 +56,7 @@ func TestFunctionRunWithoutContainer(t *testing.T) { } // Run without container (scaffolding) - knFuncTerm1.Exec("run", "--container=false", "--verbose", "--path", funcPath, "--registry", common.GetRegistry()) + knFuncTerm1.Exec("run", "--builder=host", "--verbose", "--path", funcPath, "--registry", common.GetRegistry()) }() knFuncRunCompleted := false