diff --git a/commands/init.go b/commands/init.go index 1df9bba3..94ed8784 100644 --- a/commands/init.go +++ b/commands/init.go @@ -39,6 +39,7 @@ import ( "errors" "fmt" "os" + "os/exec" "path/filepath" "sort" "strings" @@ -51,10 +52,13 @@ import ( ) type initFnCmd struct { - force bool - triggerType string - wd string - ff *common.FuncFileV20180708 + force bool + codeOnly bool + triggerType string + wd string + runtimeName string + runtimeConfigType string + ff *common.FuncFileV20180708 } func initFlags(a *initFnCmd) []cli.Flag { @@ -68,10 +72,25 @@ func initFlags(a *initFnCmd) []cli.Flag { Usage: "Overwrite existing func.yaml", Destination: &a.force, }, + cli.BoolFlag{ + Name: "code-only", + Usage: "Initialize a code-only function", + Destination: &a.codeOnly, + }, cli.StringFlag{ Name: "runtime", Usage: "Choose an existing runtime - " + langsList(), }, + cli.StringFlag{ + Name: "runtime-name", + Usage: "Specify the managed runtime name (e.g. python39.ol9) for code-only functions.", + Destination: &a.runtimeName, + }, + cli.StringFlag{ + Name: "runtime-config-type", + Usage: "Set the runtime configuration type for managed runtimes. Required for code-only functions.", + Destination: &a.runtimeConfigType, + }, cli.StringFlag{ Name: "init-image", Usage: "A Docker image which will create a function template", @@ -179,12 +198,28 @@ func (a *initFnCmd) init(c *cli.Context) error { runtime := c.String("runtime") initImage := c.String("init-image") + a.runtimeName = strings.TrimSpace(a.runtimeName) + a.runtimeConfigType = strings.TrimSpace(a.runtimeConfigType) if runtime != "" && initImage != "" { return fmt.Errorf("You can't supply --runtime with --init-image") } + if (a.codeOnly || a.runtimeName != "") && initImage != "" { + return fmt.Errorf("You can't supply --code-only with --init-image") + } + if runtime != "" && a.runtimeName != "" { + return fmt.Errorf("Specify either --runtime or --runtime-name, not both") + } + if a.codeOnly && runtime == common.FuncfileDockerRuntime { + return fmt.Errorf("Init does not support the '%s' runtime for code-only functions", runtime) + } runtimeSpecified := runtime != "" + codeOnlyInit := a.codeOnly || a.runtimeName != "" + precheckedRuntime := "" + if codeOnlyInit && a.runtimeConfigType == "" { + return fmt.Errorf("Code-only init requires --runtime-config-type") + } a.ff.Schema_version = common.LatestYamlVersion if runtimeSpecified { @@ -197,6 +232,15 @@ func (a *initFnCmd) init(c *cli.Context) error { } } + if runtime != "" { + precheckedRuntime = runtime + } else if a.runtimeName != "" { + precheckedRuntime = a.runtimeName + } + if err := precheckToolingForRuntime(precheckedRuntime); err != nil { + return err + } + path := c.Args().First() if path != "" { fmt.Printf("Creating function at: ./%s\n", path) @@ -262,9 +306,23 @@ func (a *initFnCmd) init(c *cli.Context) error { return errors.New("Function file already exists, aborting") } } - err = a.BuildFuncFileV20180708(c, dir) // TODO: Return LangHelper here, then don't need to refind the helper in generateBoilerplate() below - if err != nil { - return err + + if codeOnlyInit { + err = a.buildCodeOnlyFuncFile(c, runtime) + if err != nil { + return err + } + } else { + err = a.BuildFuncFileV20180708(c, dir) // TODO: Return LangHelper here, then don't need to refind the helper in generateBoilerplate() below + if err != nil { + return err + } + } + + if precheckedRuntime == "" { + if err := a.precheckTooling(); err != nil { + return err + } } a.ff.Schema_version = common.LatestYamlVersion @@ -277,7 +335,12 @@ func (a *initFnCmd) init(c *cli.Context) error { } else { // TODO: why don't we treat "docker" runtime as just another language helper? // Then can get rid of several Docker specific if/else's like this one. - if runtimeSpecified && runtime != common.FuncfileDockerRuntime { + if codeOnlyInit { + err := a.generateCodeOnlyBoilerplate(dir, runtime) + if err != nil { + return err + } + } else if runtimeSpecified && runtime != common.FuncfileDockerRuntime { err := a.generateBoilerplate(dir, runtime) if err != nil { return err @@ -293,6 +356,44 @@ func (a *initFnCmd) init(c *cli.Context) error { return nil } +func (a *initFnCmd) buildCodeOnlyFuncFile(c *cli.Context, runtime string) error { + a.ff.Version = c.String("version") + if err := ValidateFuncName(a.ff.Name); err != nil { + return err + } + + runtimeName := runtime + if runtimeName == "" { + runtimeName = a.runtimeName + } + if runtimeName == "" { + return fmt.Errorf("Code-only init requires --runtime-name or --runtime") + } + + a.setupCodeOnlyFuncFile(runtimeName) + return nil +} + +func (a *initFnCmd) setupCodeOnlyFuncFile(runtimeName string) { + a.ff.Code_only = true + a.ff.Runtime = "" + a.ff.Build_image = "" + a.ff.Run_image = "" + a.ff.Cmd = "" + a.ff.Entrypoint = "" + a.ff.Build = nil + + a.ff.Runtime_config = &common.RuntimeConfigV20180708{ + Type: a.runtimeConfigType, + Runtime_name: runtimeName, + Runtime_version_id: "", + } + + if requiresCodeOnlyHandler(runtimeName) { + a.ff.Handler = defaultCodeOnlyHandler(runtimeName) + } +} + func (a *initFnCmd) doInitImage(initImage string, c *cli.Context) error { err := common.RunInitImage(initImage, a.ff.Name) if err != nil { @@ -327,6 +428,258 @@ func (a *initFnCmd) generateBoilerplate(path, runtime string) error { return nil } +func (a *initFnCmd) generateCodeOnlyBoilerplate(path, runtime string) error { + runtimeName := runtime + if runtimeName == "" { + runtimeName = a.runtimeName + } + if runtimeName == "" && a.ff.Runtime_config != nil { + runtimeName = a.ff.Runtime_config.Runtime_name + } + if runtimeName == "" { + return nil + } + + baseRuntime := baseRuntimeFromName(runtimeName) + switch { + case strings.HasPrefix(baseRuntime, "python"): + generated, err := createPythonCodeOnlyBoilerplate(path) + if err != nil { + return err + } + if generated { + fmt.Println("Function boilerplate generated.") + } + case strings.HasPrefix(baseRuntime, "node"), strings.HasPrefix(baseRuntime, "javascript"): + generated, err := createNodeCodeOnlyBoilerplate(path) + if err != nil { + return err + } + if generated { + fmt.Println("Function boilerplate generated.") + } + case strings.HasPrefix(baseRuntime, "java"): + generated, err := createJavaCodeOnlyBoilerplate(path, runtimeName) + if err != nil { + return err + } + if generated { + fmt.Println("Function boilerplate generated.") + } + case strings.HasPrefix(baseRuntime, "go"): + generated, err := createGoCodeOnlyBoilerplate(path, runtimeName) + if err != nil { + return err + } + if generated { + fmt.Println("Function boilerplate generated.") + } + default: + helper := langs.GetLangHelper(runtime) + if helper == nil { + helper = langs.GetLangHelper(baseRuntime) + } + if helper != nil && helper.HasBoilerplate() { + if err := helper.GenerateBoilerplate(path); err != nil { + if err == langs.ErrBoilerplateExists { + return nil + } + return err + } + fmt.Println("Function boilerplate generated.") + } + } + + return nil +} + +func (a *initFnCmd) precheckTooling() error { + runtime := a.ff.Runtime + if a.ff.Runtime_config != nil && a.ff.Runtime_config.Runtime_name != "" { + runtime = a.ff.Runtime_config.Runtime_name + } + + return precheckToolingForRuntime(runtime) +} + + +func precheckToolingForRuntime(runtime string) error { + runtimeName, requiredTool, candidates := runtimeToolRequirement(runtime) + if requiredTool == "" { + return nil + } + + for _, candidate := range candidates { + if _, err := exec.LookPath(candidate); err == nil { + return nil + } + } + + return fmt.Errorf("%s runtime selected, but %s was not found in PATH. Install %s and rerun `fn init`, or choose a different runtime", runtimeName, requiredTool, requiredTool) +} + +func runtimeToolRequirement(runtime string) (string, string, []string) { + baseRuntime := baseRuntimeFromName(runtime) + switch { + case strings.HasPrefix(baseRuntime, "java"), strings.HasPrefix(baseRuntime, "kotlin"): + return runtimeDisplayName(runtime), "Maven", []string{"mvn"} + case strings.HasPrefix(baseRuntime, "python"): + return runtimeDisplayName(runtime), "Python", []string{"python3", "python"} + case strings.HasPrefix(baseRuntime, "ruby"): + return runtimeDisplayName(runtime), "Ruby", []string{"ruby"} + case strings.HasPrefix(baseRuntime, "go"): + return runtimeDisplayName(runtime), "Go", []string{"go"} + default: + return "", "", nil + } +} + +func runtimeDisplayName(runtime string) string { + baseRuntime := baseRuntimeFromName(runtime) + switch { + case strings.HasPrefix(baseRuntime, "java"): + return "Java" + case strings.HasPrefix(baseRuntime, "kotlin"): + return "Kotlin" + case strings.HasPrefix(baseRuntime, "python"): + return "Python" + case strings.HasPrefix(baseRuntime, "ruby"): + return "Ruby" + case strings.HasPrefix(baseRuntime, "go"): + return "Go" + default: + if runtime == "" { + return "Selected" + } + return strings.Title(baseRuntime) + } +} + +func requiresCodeOnlyHandler(runtime string) bool { + baseRuntime := baseRuntimeFromName(runtime) + return strings.HasPrefix(baseRuntime, "java") || + strings.HasPrefix(baseRuntime, "python") || + strings.HasPrefix(baseRuntime, "node") || + strings.HasPrefix(baseRuntime, "javascript") +} + +func defaultCodeOnlyHandler(runtime string) string { + baseRuntime := baseRuntimeFromName(runtime) + switch { + case strings.HasPrefix(baseRuntime, "java"): + return "com.example.fn.HelloFunction::handleRequest" + case strings.HasPrefix(baseRuntime, "python"): + return "hello_world.handler" + case strings.HasPrefix(baseRuntime, "node"), strings.HasPrefix(baseRuntime, "javascript"): + return "hello-world.handler" + default: + return "handler" + } +} + +func baseRuntimeFromName(runtimeName string) string { + lower := strings.ToLower(strings.TrimSpace(runtimeName)) + for _, sep := range []string{".", "-"} { + if idx := strings.Index(lower, sep); idx != -1 { + lower = lower[:idx] + break + } + } + return lower +} + +func createPythonCodeOnlyBoilerplate(path string) (bool, error) { + filePath := filepath.Join(path, "hello_world.py") + if _, err := os.Stat(filePath); err == nil { + return false, nil + } else if !os.IsNotExist(err) { + return false, err + } + + content := "import json\n\n" + + "def handler(context, data=None):\n" + + " name = \"World\"\n" + + " if data:\n" + + " try:\n" + + " text = data.decode() if isinstance(data, (bytes, bytearray)) else str(data)\n" + + " payload = json.loads(text)\n" + + " name = payload.get(\"name\", name)\n" + + " except Exception:\n" + + " pass\n" + + " return {\n" + + " \"message\": f\"Hello {name}\"\n" + + " }\n" + + if err := os.WriteFile(filePath, []byte(content), 0644); err != nil { + return false, err + } + + return true, nil +} + +func createNodeCodeOnlyBoilerplate(path string) (bool, error) { + filePath := filepath.Join(path, "hello-world.js") + if _, err := os.Stat(filePath); err == nil { + return false, nil + } else if !os.IsNotExist(err) { + return false, err + } + + content := "exports.handler = async function (context, data) {\n" + + " let payload = data;\n" + + " if (Buffer.isBuffer(payload)) {\n" + + " try { payload = JSON.parse(payload.toString(\"utf8\")); } catch (e) {}\n" + + " } else if (typeof payload === \"string\") {\n" + + " try { payload = JSON.parse(payload); } catch (e) {}\n" + + " }\n" + + " let name = \"World\";\n" + + " if (payload && typeof payload === \"object\") {\n" + + " name = payload.name || name;\n" + + " }\n" + + " return { message: `Hello ${name}` };\n" + + "};\n" + + if err := os.WriteFile(filePath, []byte(content), 0644); err != nil { + return false, err + } + + return true, nil +} + +func createJavaCodeOnlyBoilerplate(path, runtimeName string) (bool, error) { + helper := langs.GetLangHelper(runtimeName) + if helper == nil { + helper = langs.GetLangHelper(baseRuntimeFromName(runtimeName)) + } + if helper == nil || !helper.HasBoilerplate() { + return false, nil + } + if err := helper.GenerateBoilerplate(path); err != nil { + if err == langs.ErrBoilerplateExists { + return false, nil + } + return false, err + } + return true, nil +} + +func createGoCodeOnlyBoilerplate(path, runtimeName string) (bool, error) { + helper := langs.GetLangHelper(runtimeName) + if helper == nil { + helper = langs.GetLangHelper(baseRuntimeFromName(runtimeName)) + } + if helper == nil || !helper.HasBoilerplate() { + return false, nil + } + if err := helper.GenerateBoilerplate(path); err != nil { + if err == langs.ErrBoilerplateExists { + return false, nil + } + return false, err + } + return true, nil +} + func (a *initFnCmd) bindFn(fn *modelsV2.Fn) { ff := a.ff if fn.Memory > 0 { diff --git a/common/funcfile.go b/common/funcfile.go index e6f29994..2144ff99 100644 --- a/common/funcfile.go +++ b/common/funcfile.go @@ -115,6 +115,7 @@ type FuncFileV20180708 struct { Name string `yaml:"name,omitempty" json:"name,omitempty"` Version string `yaml:"version,omitempty" json:"version,omitempty"` + Code_only bool `yaml:"code_only,omitempty" json:"code_only,omitempty"` Runtime string `yaml:"runtime,omitempty" json:"runtime,omitempty"` Build_image string `yaml:"build_image,omitempty" json:"build_image,omitempty"` // Image to use as base for building Run_image string `yaml:"run_image,omitempty" json:"run_image,omitempty"` // Image to use for running @@ -133,10 +134,19 @@ type FuncFileV20180708 struct { Build []string `yaml:"build,omitempty" json:"build,omitempty"` + Runtime_config *RuntimeConfigV20180708 `yaml:"runtime_config,omitempty" json:"runtime_config,omitempty"` + Handler string `yaml:"handler,omitempty" json:"handler,omitempty"` + Expects Expects `yaml:"expects,omitempty" json:"expects,omitempty"` Triggers []Trigger `yaml:"triggers,omitempty" json:"triggers,omitempty"` } +type RuntimeConfigV20180708 struct { + Type string `yaml:"type,omitempty" json:"type,omitempty"` + Runtime_name string `yaml:"runtime_name,omitempty" json:"runtime_name,omitempty"` + Runtime_version_id string `yaml:"runtime_version_id,omitempty" json:"runtime_version_id,omitempty"` +} + // Trigger represents a trigger for a FuncFileV20180708 type Trigger struct { Name string `yaml:"name,omitempty" json:"name,omitempty"` @@ -311,7 +321,7 @@ func ParseFuncFileV20180708(path string) (ff *FuncFileV20180708, err error) { return nil, errUnexpectedFileFormat } - if err == nil && ff.Schema_version != V20180708 { + if err == nil && ff.Schema_version != V20180708 && ff.Schema_version != V20260325 { // todo: we should maybe not assume this, but it's more useful than saying 'version mismatch' for users... return nil, fmt.Errorf("unsupported func.yaml version, please use the migrate command to update your function metadata") } diff --git a/common/schema.go b/common/schema.go index 02d6f188..da6e34d1 100644 --- a/common/schema.go +++ b/common/schema.go @@ -26,7 +26,8 @@ import ( const ( V20180708 = 20180708 - LatestYamlVersion = V20180708 + V20260325 = 20260325 + LatestYamlVersion = V20260325 ) const V20180708Schema = `{ @@ -89,6 +90,86 @@ const V20180708Schema = `{ } }` +const V20260325Schema = `{ + "title": "V20260325 func file schema", + "type": "object", + "properties": { + "name": { + "type":"string" + }, + "schema_version": { + "type":"integer" + }, + "version": { + "type":"string" + }, + "runtime": { + "type":"string" + }, + "build_image": { + "type":"string" + }, + "run_image": { + "type": "string" + }, + "entrypoint": { + "type":"string" + }, + "content_type": { + "type":"string" + }, + "cmd": { + "type":"string" + }, + "memory": { + "type":"integer" + }, + "timeout": { + "type":"integer" + }, + "idle_timeout": { + "type": "integer" + }, + "config": { + "type": "object" + }, + "code_only": { + "type": "boolean" + }, + "runtime_config": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "runtime_name": { + "type": "string" + }, + "runtime_version_id": { + "type": "string" + } + } + }, + "handler": { + "type": "string" + }, + "triggers": { + "type": "array", + "properties": { + "name": { + "type":"string" + }, + "type": { + "type":"string" + }, + "source": { + "type":"string" + } + } + } + } +}` + func ValidateFileAgainstSchema(jsonFile, schema string) error { schemaLoader := gojsonschema.NewStringLoader(schema) documentLoader := gojsonschema.NewReferenceLoader(filepath.Join("file://", GetWd(), jsonFile)) diff --git a/objects/runtime/commands.go b/objects/runtime/commands.go new file mode 100644 index 00000000..1d536c26 --- /dev/null +++ b/objects/runtime/commands.go @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +import ( + "github.com/fnproject/cli/client" + "github.com/fnproject/fn_go/provider" + "github.com/fnproject/fn_go/provider/oracle" + "github.com/urfave/cli" +) + +// List runtimes command +func ListRuntimes() cli.Command { + cmd := runtimeCmd{} + return cli.Command{ + Name: "runtimes", + Usage: "List supported runtimes", + Category: "MANAGEMENT COMMAND", + Description: "This command lists all supported runtimes.", + Before: func(c *cli.Context) error { + var err error + cmd.provider, err = client.CurrentProvider() + if err != nil { + return err + } + cmd.providerName = c.String("provider") + return nil + }, + Action: cmd.listRuntimes, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "output", + Usage: "Output format (json)", + }, + cli.StringFlag{ + Name: "provider", + Usage: "Override provider name", + }, + }, + BashComplete: func(c *cli.Context) { + provider, err := client.CurrentProvider() + if err != nil { + return + } + if _, ok := provider.(*oracle.OracleProvider); !ok { + return + } + }, + } +} + +// List runtime versions command +func ListRuntimeVersions() cli.Command { + cmd := runtimeCmd{} + return cli.Command{ + Name: "runtime-versions", + Usage: "List runtime versions for a runtime", + Category: "MANAGEMENT COMMAND", + Description: "This command lists runtime versions for a runtime name.", + Before: func(c *cli.Context) error { + var err error + cmd.provider, err = client.CurrentProvider() + if err != nil { + return err + } + cmd.providerName = c.String("provider") + return nil + }, + Action: cmd.listRuntimeVersions, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "runtime-name", + Usage: "Runtime name (e.g. java21.ol10)", + }, + cli.StringFlag{ + Name: "output", + Usage: "Output format (json)", + }, + cli.StringFlag{ + Name: "provider", + Usage: "Override provider name", + }, + }, + BashComplete: func(c *cli.Context) { + if c.String("runtime-name") != "" { + return + } + suggestRuntimeNames(c) + }, + } +} + +// Get latest runtime version command +func GetLatestRuntimeVersion() cli.Command { + cmd := runtimeCmd{} + return cli.Command{ + Name: "latest-runtime-version", + Usage: "Get the latest runtime version for a runtime", + Category: "MANAGEMENT COMMAND", + Description: "This command gets the latest runtime version for a runtime name.", + Before: func(c *cli.Context) error { + var err error + cmd.provider, err = client.CurrentProvider() + if err != nil { + return err + } + cmd.providerName = c.String("provider") + return nil + }, + Action: cmd.getLatestRuntimeVersion, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "runtime-name", + Usage: "Runtime name (e.g. java21.ol10)", + }, + cli.StringFlag{ + Name: "output", + Usage: "Output format (json)", + }, + cli.StringFlag{ + Name: "provider", + Usage: "Override provider name", + }, + }, + BashComplete: func(c *cli.Context) { + if c.String("runtime-name") != "" { + return + } + suggestRuntimeNames(c) + }, + } +} + +type runtimeCmd struct { + provider provider.Provider + providerName string +} \ No newline at end of file diff --git a/objects/runtime/runtime.go b/objects/runtime/runtime.go new file mode 100644 index 00000000..01b79633 --- /dev/null +++ b/objects/runtime/runtime.go @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + "text/tabwriter" + + "github.com/fnproject/cli/client" + "github.com/fnproject/fn_go/provider/oracle" + ociCommon "github.com/oracle/oci-go-sdk/v65/common" + "github.com/oracle/oci-go-sdk/v65/functions" + "github.com/urfave/cli" +) + +func (c *runtimeCmd) ensureOracleProvider() (*oracle.OracleProvider, error) { + ociProvider, ok := c.provider.(*oracle.OracleProvider) + if !ok || ociProvider == nil { + return nil, fmt.Errorf("runtime discovery requires an oracle provider") + } + return ociProvider, nil +} + +func (c *runtimeCmd) listRuntimes(cliCtx *cli.Context) error { + ociProvider, err := c.ensureOracleProvider() + if err != nil { + return err + } + + client, err := newFunctionsClient(ociProvider) + if err != nil { + return err + } + + request := functions.ListFunctionsRuntimesRequest{} + var items []functions.FunctionsRuntimeSummary + for { + response, err := client.ListFunctionsRuntimes(context.Background(), request) + if err != nil { + return err + } + items = append(items, response.Items...) + if response.OpcNextPage == nil { + break + } + request.Page = response.OpcNextPage + } + + return printRuntimes(cliCtx, items) +} + +func (c *runtimeCmd) listRuntimeVersions(cliCtx *cli.Context) error { + runtimeName := strings.TrimSpace(cliCtx.String("runtime-name")) + if runtimeName == "" { + return fmt.Errorf("--runtime-name is required") + } + + ociProvider, err := c.ensureOracleProvider() + if err != nil { + return err + } + + client, err := newFunctionsClient(ociProvider) + if err != nil { + return err + } + + request := functions.ListFunctionsRuntimeVersionsRequest{ + FunctionsRuntimeName: &runtimeName, + } + var items []functions.FunctionsRuntimeVersionSummary + for { + response, err := client.ListFunctionsRuntimeVersions(context.Background(), request) + if err != nil { + return err + } + items = append(items, response.Items...) + if response.OpcNextPage == nil { + break + } + request.Page = response.OpcNextPage + } + + return printRuntimeVersions(cliCtx, items) +} + +func (c *runtimeCmd) getLatestRuntimeVersion(cliCtx *cli.Context) error { + runtimeName := strings.TrimSpace(cliCtx.String("runtime-name")) + if runtimeName == "" { + return fmt.Errorf("--runtime-name is required") + } + + ociProvider, err := c.ensureOracleProvider() + if err != nil { + return err + } + + client, err := newFunctionsClient(ociProvider) + if err != nil { + return err + } + + request := functions.ListFunctionsRuntimeVersionsRequest{ + FunctionsRuntimeName: &runtimeName, + IsCurrentVersion: ociCommon.Bool(true), + Limit: ociCommon.Int(1), + } + response, err := client.ListFunctionsRuntimeVersions(context.Background(), request) + if err != nil { + return err + } + if len(response.Items) == 0 { + return fmt.Errorf("no runtime versions found for runtime %s", runtimeName) + } + return printLatestRuntimeVersion(cliCtx, response.Items[0]) +} + +func printRuntimes(cliCtx *cli.Context, items []functions.FunctionsRuntimeSummary) error { + outputFormat := strings.ToLower(cliCtx.String("output")) + if outputFormat == "json" { + b, err := json.MarshalIndent(items, "", " ") + if err != nil { + return err + } + fmt.Fprint(os.Stdout, string(b)) + return nil + } + + w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0) + fmt.Fprint(w, "NAME", "\t", "LANGUAGE", "\t", "OS", "\t", "STATE", "\t", "CURRENT_VERSION_ID", "\n") + for _, item := range items { + fmt.Fprint(w, + stringValue(item.Name), "\t", + stringValue(item.Language), "\t", + stringValue(item.Os), "\t", + item.LifecycleState, "\t", + stringValue(item.CurrentFunctionsRuntimeVersionId), "\t", + "\n", + ) + } + return w.Flush() +} + +func printRuntimeVersions(cliCtx *cli.Context, items []functions.FunctionsRuntimeVersionSummary) error { + outputFormat := strings.ToLower(cliCtx.String("output")) + if outputFormat == "json" { + b, err := json.MarshalIndent(items, "", " ") + if err != nil { + return err + } + fmt.Fprint(os.Stdout, string(b)) + return nil + } + + w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0) + fmt.Fprint(w, "DISPLAY_NAME", "\t", "LANGUAGE_VERSION", "\t", "OS_VERSION", "\t", "STATE", "\t", "ID", "\n") + for _, item := range items { + fmt.Fprint(w, + stringValue(item.DisplayName), "\t", + stringValue(item.LanguageVersion), "\t", + stringValue(item.OsVersion), "\t", + item.LifecycleState, "\t", + stringValue(item.Id), "\t", + "\n", + ) + } + return w.Flush() +} + +func printLatestRuntimeVersion(cliCtx *cli.Context, item functions.FunctionsRuntimeVersionSummary) error { + outputFormat := strings.ToLower(cliCtx.String("output")) + if outputFormat == "json" { + b, err := json.MarshalIndent(item, "", " ") + if err != nil { + return err + } + fmt.Fprint(os.Stdout, string(b)) + return nil + } + + w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0) + fmt.Fprint(w, "DISPLAY_NAME", "\t", "LANGUAGE_VERSION", "\t", "OS_VERSION", "\t", "STATE", "\t", "ID", "\n") + fmt.Fprint(w, + stringValue(item.DisplayName), "\t", + stringValue(item.LanguageVersion), "\t", + stringValue(item.OsVersion), "\t", + item.LifecycleState, "\t", + stringValue(item.Id), "\t", + "\n", + ) + return w.Flush() +} + +func suggestRuntimeNames(cliCtx *cli.Context) { + provider, err := client.CurrentProvider() + if err != nil { + return + } + ociProvider, ok := provider.(*oracle.OracleProvider) + if !ok || ociProvider == nil { + return + } + client, err := newFunctionsClient(ociProvider) + if err != nil { + return + } + + request := functions.ListFunctionsRuntimesRequest{} + for { + response, err := client.ListFunctionsRuntimes(context.Background(), request) + if err != nil { + return + } + for _, item := range response.Items { + name := stringValue(item.Name) + if name != "" { + fmt.Println(name) + } + } + if response.OpcNextPage == nil { + break + } + request.Page = response.OpcNextPage + } +} + +func getRegion(oracleProvider *oracle.OracleProvider) string { + if oracleProvider.FnApiUrl != nil { + parts := strings.Split(oracleProvider.FnApiUrl.Host, ".") + if len(parts) >= 4 { + return parts[1] + } + } + region, _ := oracleProvider.ConfigurationProvider.Region() + return region +} + +func newFunctionsClient(oracleProvider *oracle.OracleProvider) (functions.FunctionsManagementClient, error) { + client, err := functions.NewFunctionsManagementClientWithConfigurationProvider(oracleProvider.ConfigurationProvider) + if err != nil { + return client, err + } + if oracleProvider.FnApiUrl != nil { + client.Host = oracleProvider.FnApiUrl.String() + return client, nil + } + client.SetRegion(getRegion(oracleProvider)) + return client, nil +} + +func stringValue(value *string) string { + if value == nil { + return "" + } + return *value +} \ No newline at end of file diff --git a/test/cli_code_only_init_test.go b/test/cli_code_only_init_test.go new file mode 100644 index 00000000..793da049 --- /dev/null +++ b/test/cli_code_only_init_test.go @@ -0,0 +1,109 @@ +package test + +import ( + "testing" + + "github.com/fnproject/cli/common" + "github.com/fnproject/cli/testharness" +) + +func TestCodeOnlyInit(t *testing.T) { + t.Run("`fn init --code-only --runtime-name python311.ol9` should generate code-only func.yaml and python boilerplate", func(t *testing.T) { + t.Parallel() + h := testharness.Create(t) + defer h.Cleanup() + + appName := h.NewAppName() + funcName := h.NewFuncName(appName) + dirName := funcName + "_dir" + h.Fn("init", "--code-only", "--runtime-name", "python311.ol9", "--runtime-config-type", "function-update", "--name", funcName, dirName).AssertSuccess() + + h.Cd(dirName) + yamlFile := h.GetYamlFile("func.yaml") + + if yamlFile.Schema_version != common.LatestYamlVersion { + t.Fatalf("schema_version was %d, expected %d", yamlFile.Schema_version, common.LatestYamlVersion) + } + if !yamlFile.Code_only { + t.Fatal("code_only was not set in func.yaml") + } + if yamlFile.Runtime_config == nil { + t.Fatal("runtime_config was not set in func.yaml") + } + if yamlFile.Runtime_config.Type != "function-update" { + t.Fatalf("runtime_config.type was %q, expected function-update", yamlFile.Runtime_config.Type) + } + if yamlFile.Runtime_config.Runtime_name != "python311.ol9" { + t.Fatalf("runtime_config.runtime_name was %q, expected python311.ol9", yamlFile.Runtime_config.Runtime_name) + } + if yamlFile.Handler != "hello_world.handler" { + t.Fatalf("handler was %q, expected hello_world.handler", yamlFile.Handler) + } + if yamlFile.Build_image != "" || yamlFile.Run_image != "" { + t.Fatal("code-only func.yaml should not contain build_image or run_image") + } + if yamlFile.Runtime != "" { + t.Fatal("code-only func.yaml should not contain runtime") + } + if h.GetFile("hello_world.py") == "" { + t.Fatal("expected hello_world.py boilerplate to be generated") + } + }) + + t.Run("`fn init --code-only --runtime go` should generate code-only func.yaml and go boilerplate", func(t *testing.T) { + t.Parallel() + h := testharness.Create(t) + defer h.Cleanup() + + appName := h.NewAppName() + funcName := h.NewFuncName(appName) + dirName := funcName + "_dir" + h.Fn("init", "--code-only", "--runtime", "go", "--runtime-config-type", "function-update", "--name", funcName, dirName).AssertSuccess() + + h.Cd(dirName) + yamlFile := h.GetYamlFile("func.yaml") + + if !yamlFile.Code_only { + t.Fatal("code_only was not set in func.yaml") + } + if yamlFile.Runtime_config == nil { + t.Fatal("runtime_config was not set in func.yaml") + } + if yamlFile.Runtime_config.Runtime_name != "go" { + t.Fatalf("runtime_config.runtime_name was %q, expected go", yamlFile.Runtime_config.Runtime_name) + } + if yamlFile.Handler != "" { + t.Fatalf("handler was %q, expected empty for go", yamlFile.Handler) + } + if h.GetFile("func.go") == "" { + t.Fatal("expected func.go boilerplate to be generated") + } + }) + + t.Run("`fn init --code-only --runtime java` should require Maven", func(t *testing.T) { + t.Parallel() + h := testharness.Create(t) + defer h.Cleanup() + + h.WithEnv("PATH", "/usr/bin:/bin") + h.Fn("init", "--code-only", "--runtime", "java", "--runtime-config-type", "function-update", "hello-java").AssertFailed().AssertStderrContains("Maven was not found in PATH") + }) +} + +func TestRuntimeDiscoveryArgValidation(t *testing.T) { + t.Run("`fn list runtime-versions` should require runtime-name", func(t *testing.T) { + t.Parallel() + h := testharness.Create(t) + defer h.Cleanup() + + h.Fn("list", "runtime-versions").AssertFailed().AssertStderrContains("--runtime-name is required") + }) + + t.Run("`fn get latest-runtime-version` should require runtime-name", func(t *testing.T) { + t.Parallel() + h := testharness.Create(t) + defer h.Cleanup() + + h.Fn("get", "latest-runtime-version").AssertFailed().AssertStderrContains("--runtime-name is required") + }) +} \ No newline at end of file