From 336ba3bb858fb2fea29010f5898f8df238fc3f28 Mon Sep 17 00:00:00 2001 From: jayarora Date: Wed, 6 May 2026 12:50:55 +0100 Subject: [PATCH] Add provisioned concurrency support to Fn CLI --- commands/deploy.go | 18 ++ commands/init.go | 9 + common/provisioned_concurrency.go | 84 +++++++ common/provisioned_concurrency_test.go | 54 +++++ objects/fn/fns.go | 220 ++++++++++++++++-- objects/fn/fns_test.go | 91 ++++++++ test/cli_init_test.go | 25 ++ .../oracle/shim/client/client_mock.go | 4 + .../fn_go/provider/oracle/shim/fns.go | 72 ++++++ 9 files changed, 559 insertions(+), 18 deletions(-) create mode 100644 common/provisioned_concurrency.go create mode 100644 common/provisioned_concurrency_test.go create mode 100644 objects/fn/fns_test.go diff --git a/commands/deploy.go b/commands/deploy.go index ca85fd9c..cfdd2b0e 100644 --- a/commands/deploy.go +++ b/commands/deploy.go @@ -97,6 +97,7 @@ func DeployCommand() cli.Command { if err != nil { return err } + cmd.provider = provider cmd.clientV2 = provider.APIClientv2() return nil }, @@ -422,19 +423,31 @@ func (p *deploycmd) deployFuncV20180708(c *cli.Context, app *models.App, funcfil func (p *deploycmd) updateFunction(c *cli.Context, appID string, ff *common.FuncFileV20180708) error { fmt.Printf("Updating function %s using image %s...\n", ff.Name, ff.ImageNameV20180708()) + if ff.Deploy != nil && ff.Deploy.OCI != nil && ff.Deploy.OCI.ProvisionedConcurrency != nil { + if err := common.ValidateProvisionedConcurrencyConfig(ff.Deploy.OCI.ProvisionedConcurrency); err != nil { + return err + } + } fn := &models.Fn{} if err := function.WithFuncFileV20180708(ff, fn); err != nil { return fmt.Errorf("Error getting function with funcfile: %s", err) } + created := false fnRes, err := function.GetFnByName(p.clientV2, appID, ff.Name) if _, ok := err.(function.NameNotFoundError); ok { fn.Name = ff.Name + if ff.Deploy != nil && ff.Deploy.OCI != nil && ff.Deploy.OCI.ProvisionedConcurrency != nil && common.IsOracleProvider(p.provider) { + if err := function.SetProvisionedConcurrencyAnnotations(fn, ff.Deploy.OCI.ProvisionedConcurrency); err != nil { + return err + } + } fn, err = function.CreateFn(p.clientV2, appID, fn) if err != nil { return err } + created = true } else if err != nil { // probably service is down or something... return err @@ -473,6 +486,11 @@ func (p *deploycmd) updateFunction(c *cli.Context, appID string, ff *common.Func } } } + if !created && ff.Deploy != nil && ff.Deploy.OCI != nil && ff.Deploy.OCI.ProvisionedConcurrency != nil && common.IsOracleProvider(p.provider) { + if err := function.ApplyProvisionedConcurrency(p.provider, fn.ID, ff.Deploy.OCI.ProvisionedConcurrency); err != nil { + return err + } + } return nil } diff --git a/commands/init.go b/commands/init.go index 1df9bba3..c9043dfc 100644 --- a/commands/init.go +++ b/commands/init.go @@ -119,6 +119,10 @@ func initFlags(a *initFnCmd) []cli.Flag { Name: "annotation", Usage: "Function annotation (can be specified multiple times)", }, + cli.StringFlag{ + Name: "provisioned-concurrency", + Usage: "Set OCI provisioned concurrency using 'none' or 'constant:'", + }, } return fgs @@ -176,6 +180,11 @@ func (a *initFnCmd) init(c *cli.Context) error { function.WithFlags(c, &fn) a.bindFn(&fn) + pcConfig, err := common.ParseProvisionedConcurrencySpec(c.String("provisioned-concurrency")) + if err != nil { + return err + } + common.SetProvisionedConcurrency(a.ff, pcConfig) runtime := c.String("runtime") initImage := c.String("init-image") diff --git a/common/provisioned_concurrency.go b/common/provisioned_concurrency.go new file mode 100644 index 00000000..cdd076f6 --- /dev/null +++ b/common/provisioned_concurrency.go @@ -0,0 +1,84 @@ +package common + +import ( + "fmt" + "strconv" + "strings" +) + +const ( + ProvisionedConcurrencyStrategyNone = "NONE" + ProvisionedConcurrencyStrategyConstant = "CONSTANT" + ProvisionedConcurrencyCountStep = 40 +) + +// ValidateProvisionedConcurrencyConfig validates OCI provisioned concurrency values +// before they are sent to OCI. +func ValidateProvisionedConcurrencyConfig(cfg *OCIProvisionedConcurrencyConfig) error { + if cfg == nil { + return nil + } + + switch strings.ToUpper(strings.TrimSpace(cfg.Strategy)) { + case ProvisionedConcurrencyStrategyNone: + return nil + case ProvisionedConcurrencyStrategyConstant: + if cfg.Count == nil || *cfg.Count <= 0 { + return fmt.Errorf("provisioned concurrency count must be a positive integer") + } + if *cfg.Count%ProvisionedConcurrencyCountStep != 0 { + return fmt.Errorf("provisioned concurrency count must be a multiple of %d", ProvisionedConcurrencyCountStep) + } + return nil + default: + return fmt.Errorf("unsupported provisioned concurrency strategy %q", cfg.Strategy) + } +} + +// ParseProvisionedConcurrencySpec parses a curated provisioned concurrency CLI value. +// Supported values are: +// - none +// - constant: +func ParseProvisionedConcurrencySpec(spec string) (*OCIProvisionedConcurrencyConfig, error) { + spec = strings.TrimSpace(spec) + if spec == "" { + return nil, nil + } + + if strings.EqualFold(spec, "none") { + return &OCIProvisionedConcurrencyConfig{Strategy: ProvisionedConcurrencyStrategyNone}, nil + } + + parts := strings.SplitN(spec, ":", 2) + if len(parts) != 2 || !strings.EqualFold(strings.TrimSpace(parts[0]), "constant") { + return nil, fmt.Errorf("invalid value for --provisioned-concurrency: %q (expected 'none' or 'constant:')", spec) + } + + count, err := strconv.Atoi(strings.TrimSpace(parts[1])) + if err != nil || count <= 0 { + return nil, fmt.Errorf("invalid value for --provisioned-concurrency: %q (count must be a positive integer)", spec) + } + + cfg := &OCIProvisionedConcurrencyConfig{ + Strategy: ProvisionedConcurrencyStrategyConstant, + Count: &count, + } + if err := ValidateProvisionedConcurrencyConfig(cfg); err != nil { + return nil, fmt.Errorf("invalid value for --provisioned-concurrency: %q (%s)", spec, err) + } + return cfg, nil +} + +// SetProvisionedConcurrency stores provisioned concurrency config in the function deploy section. +func SetProvisionedConcurrency(ff *FuncFileV20180708, cfg *OCIProvisionedConcurrencyConfig) { + if ff == nil || cfg == nil { + return + } + if ff.Deploy == nil { + ff.Deploy = &FuncDeployConfig{} + } + if ff.Deploy.OCI == nil { + ff.Deploy.OCI = &OCIFunctionDeployConfig{} + } + ff.Deploy.OCI.ProvisionedConcurrency = cfg +} \ No newline at end of file diff --git a/common/provisioned_concurrency_test.go b/common/provisioned_concurrency_test.go new file mode 100644 index 00000000..87265f5a --- /dev/null +++ b/common/provisioned_concurrency_test.go @@ -0,0 +1,54 @@ +package common + +import "testing" + +func TestParseProvisionedConcurrencySpec(t *testing.T) { + tests := []struct { + name string + spec string + wantNil bool + wantErr bool + strategy string + wantCount int + }{ + {name: "empty", spec: "", wantNil: true}, + {name: "none", spec: "none", strategy: ProvisionedConcurrencyStrategyNone}, + {name: "none case insensitive", spec: "NoNe", strategy: ProvisionedConcurrencyStrategyNone}, + {name: "constant", spec: "constant:40", strategy: ProvisionedConcurrencyStrategyConstant, wantCount: 40}, + {name: "constant with spaces", spec: " constant:80 ", strategy: ProvisionedConcurrencyStrategyConstant, wantCount: 80}, + {name: "invalid missing count", spec: "constant", wantErr: true}, + {name: "invalid non positive", spec: "constant:0", wantErr: true}, + {name: "invalid non numeric", spec: "constant:abc", wantErr: true}, + {name: "invalid non multiple", spec: "constant:5", wantErr: true}, + {name: "invalid strategy", spec: "foo:1", wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg, err := ParseProvisionedConcurrencySpec(tt.spec) + if (err != nil) != tt.wantErr { + t.Fatalf("ParseProvisionedConcurrencySpec() error = %v, wantErr %v", err, tt.wantErr) + } + if tt.wantNil { + if cfg != nil { + t.Fatalf("expected nil config, got %#v", cfg) + } + return + } + if tt.wantErr { + return + } + if cfg == nil { + t.Fatal("expected non-nil config") + } + if cfg.Strategy != tt.strategy { + t.Fatalf("expected strategy %q, got %q", tt.strategy, cfg.Strategy) + } + if tt.strategy == ProvisionedConcurrencyStrategyConstant { + if cfg.Count == nil || *cfg.Count != tt.wantCount { + t.Fatalf("expected count %d, got %#v", tt.wantCount, cfg.Count) + } + } + }) + } +} \ No newline at end of file diff --git a/objects/fn/fns.go b/objects/fn/fns.go index 2221605e..ff7a05ca 100644 --- a/objects/fn/fns.go +++ b/objects/fn/fns.go @@ -35,6 +35,8 @@ import ( models "github.com/fnproject/fn_go/modelsv2" "github.com/fnproject/fn_go/provider" "github.com/jmoiron/jsonq" + fnprovideroracle "github.com/fnproject/fn_go/provider/oracle" + ocifunctions "github.com/oracle/oci-go-sdk/v65/functions" "github.com/urfave/cli" ) @@ -43,6 +45,38 @@ type fnsCmd struct { client *fnclient.Fn } +const ( + annotationProvisionedConcurrencyStrategy = "oracle.com/oci/provisionedConcurrencyStrategy" + annotationProvisionedConcurrencyCount = "oracle.com/oci/provisionedConcurrencyCount" +) + +// SetProvisionedConcurrencyAnnotations adds the internal annotations used to +// carry provisioned concurrency through the create payload into the OCI shim. +func SetProvisionedConcurrencyAnnotations(fn *models.Fn, cfg *common.OCIProvisionedConcurrencyConfig) error { + if fn == nil || cfg == nil { + return nil + } + if err := common.ValidateProvisionedConcurrencyConfig(cfg); err != nil { + return err + } + if fn.Annotations == nil { + fn.Annotations = make(map[string]interface{}) + } + strategy := strings.ToUpper(strings.TrimSpace(cfg.Strategy)) + fn.Annotations[annotationProvisionedConcurrencyStrategy] = strategy + if strategy == common.ProvisionedConcurrencyStrategyConstant && cfg.Count != nil { + fn.Annotations[annotationProvisionedConcurrencyCount] = *cfg.Count + } else { + delete(fn.Annotations, annotationProvisionedConcurrencyCount) + } + return nil +} + +type provisionedConcurrencyView struct { + Strategy string `json:"strategy"` + Count *int `json:"count,omitempty"` +} + // FnFlags used to create/update functions var FnFlags = []cli.Flag{ cli.Uint64Flag{ @@ -69,6 +103,10 @@ var FnFlags = []cli.Flag{ Name: "image", Usage: "Function image", }, + cli.StringFlag{ + Name: "provisioned-concurrency", + Usage: "Set OCI provisioned concurrency using 'none' or 'constant:'", + }, } var updateFnFlags = FnFlags @@ -95,13 +133,15 @@ func printFunctions(c *cli.Context, fns []*models.Fn) error { var newFns []interface{} for _, fn := range fns { newFns = append(newFns, struct { - Name string `json:"name"` - Image string `json:"image"` - ID string `json:"id"` + Name string `json:"name"` + Image string `json:"image"` + ID string `json:"id"` + ProvisionedConcurrency *provisionedConcurrencyView `json:"provisionedConcurrency,omitempty"` }{ - fn.Name, - fn.Image, - fn.ID, + Name: fn.Name, + Image: fn.Image, + ID: fn.ID, + ProvisionedConcurrency: getProvisionedConcurrencyView(fn), }) } b, err := json.MarshalIndent(newFns, "", " ") @@ -111,10 +151,10 @@ func printFunctions(c *cli.Context, fns []*models.Fn) error { fmt.Fprint(os.Stdout, string(b)) } else { w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0) - fmt.Fprint(w, "NAME", "\t", "IMAGE", "\t", "ID", "\n") + fmt.Fprint(w, "NAME", "\t", "IMAGE", "\t", "PC", "\t", "ID", "\n") for _, f := range fns { - fmt.Fprint(w, f.Name, "\t", f.Image, "\t", f.ID, "\t", "\n") + fmt.Fprint(w, f.Name, "\t", f.Image, "\t", formatProvisionedConcurrencyDisplay(f), "\t", f.ID, "\t", "\n") } if err := w.Flush(); err != nil { return err @@ -123,6 +163,71 @@ func printFunctions(c *cli.Context, fns []*models.Fn) error { return nil } +func getProvisionedConcurrencyView(fn *models.Fn) *provisionedConcurrencyView { + if fn == nil || fn.Annotations == nil { + return nil + } + strategyRaw, ok := fn.Annotations[annotationProvisionedConcurrencyStrategy] + if !ok { + return nil + } + strategy, ok := strategyRaw.(string) + if !ok || strings.TrimSpace(strategy) == "" { + return nil + } + view := &provisionedConcurrencyView{Strategy: strings.ToUpper(strategy)} + if countRaw, ok := fn.Annotations[annotationProvisionedConcurrencyCount]; ok { + switch typed := countRaw.(type) { + case int: + count := typed + view.Count = &count + case int32: + count := int(typed) + view.Count = &count + case int64: + count := int(typed) + view.Count = &count + case float64: + count := int(typed) + view.Count = &count + } + } + return view +} + +func formatProvisionedConcurrencyDisplay(fn *models.Fn) string { + view := getProvisionedConcurrencyView(fn) + if view == nil { + return "" + } + switch strings.ToUpper(view.Strategy) { + case "NONE": + return "none" + case "CONSTANT": + if view.Count == nil { + return "constant" + } + return fmt.Sprintf("constant:%d", *view.Count) + default: + return strings.ToLower(view.Strategy) + } +} + +func buildInspectFnMap(fn *models.Fn) (map[string]interface{}, error) { + data, err := json.Marshal(fn) + if err != nil { + return nil, err + } + inspect := map[string]interface{}{} + if err := json.Unmarshal(data, &inspect); err != nil { + return nil, err + } + if pc := getProvisionedConcurrencyView(fn); pc != nil { + inspect["provisionedConcurrency"] = pc + } + return inspect, nil +} + func (f *fnsCmd) list(c *cli.Context) error { resFns, err := getFns(c, f.client) if err != nil { @@ -258,9 +363,72 @@ func WithFuncFileV20180708(ff *common.FuncFileV20180708, fn *models.Fn) error { return nil } +func warnUnsupportedProvisionedConcurrency() { + fmt.Fprintln(os.Stderr, "Warning: --provisioned-concurrency is only supported with an oracle provider and will be ignored.") +} + +func buildFunctionsManagementClient(oracleProvider *fnprovideroracle.OracleProvider) (*ocifunctions.FunctionsManagementClient, error) { + client, err := ocifunctions.NewFunctionsManagementClientWithConfigurationProvider(oracleProvider.ConfigurationProvider) + if err != nil { + return nil, err + } + if oracleProvider.FnApiUrl != nil { + client.Host = oracleProvider.FnApiUrl.String() + } else { + region, _ := oracleProvider.ConfigurationProvider.Region() + if region != "" { + client.SetRegion(region) + } + } + return &client, nil +} + +// ApplyProvisionedConcurrency applies OCI provisioned concurrency to a function when the active provider is Oracle. +func ApplyProvisionedConcurrency(p provider.Provider, fnID string, cfg *common.OCIProvisionedConcurrencyConfig) error { + if p == nil || cfg == nil || fnID == "" { + return nil + } + if err := common.ValidateProvisionedConcurrencyConfig(cfg); err != nil { + return err + } + oracleProvider, ok := p.(*fnprovideroracle.OracleProvider) + if !ok || oracleProvider == nil { + return nil + } + mgmtClient, err := buildFunctionsManagementClient(oracleProvider) + if err != nil { + return err + } + + var pcConfig ocifunctions.FunctionProvisionedConcurrencyConfig + switch strings.ToUpper(cfg.Strategy) { + case common.ProvisionedConcurrencyStrategyNone: + pcConfig = ocifunctions.NoneProvisionedConcurrencyConfig{} + case common.ProvisionedConcurrencyStrategyConstant: + if cfg.Count == nil { + return fmt.Errorf("provisioned concurrency count is required for CONSTANT strategy") + } + pcConfig = ocifunctions.ConstantProvisionedConcurrencyConfig{Count: cfg.Count} + default: + return fmt.Errorf("unsupported provisioned concurrency strategy %q", cfg.Strategy) + } + + _, err = mgmtClient.UpdateFunction(context.Background(), ocifunctions.UpdateFunctionRequest{ + FunctionId: &fnID, + UpdateFunctionDetails: ocifunctions.UpdateFunctionDetails{ + ProvisionedConcurrencyConfig: pcConfig, + }, + }) + return err +} + func (f *fnsCmd) create(c *cli.Context) error { appName := c.Args().Get(0) fnName := c.Args().Get(1) + pcConfig, err := common.ParseProvisionedConcurrencySpec(c.String("provisioned-concurrency")) + if err != nil { + return err + } fn := &models.Fn{} fn.Name = fnName @@ -274,6 +442,13 @@ func (f *fnsCmd) create(c *cli.Context) error { if fn.Image == "" { return errors.New("no image specified") } + if pcConfig != nil { + if !common.IsOracleProvider(f.provider) { + warnUnsupportedProvisionedConcurrency() + } else if err := SetProvisionedConcurrencyAnnotations(fn, pcConfig); err != nil { + return err + } + } a, err := app.GetAppByName(f.client, appName) if err != nil { @@ -375,6 +550,13 @@ func GetFnByName(client *fnclient.Fn, appID, fnName string) (*models.Fn, error) func (f *fnsCmd) update(c *cli.Context) error { appName := c.Args().Get(0) fnName := c.Args().Get(1) + pcConfig, err := common.ParseProvisionedConcurrencySpec(c.String("provisioned-concurrency")) + if err != nil { + return err + } + if err := common.ValidateProvisionedConcurrencyConfig(pcConfig); err != nil { + return err + } app, err := app.GetAppByName(f.client, appName) if err != nil { @@ -391,6 +573,13 @@ func (f *fnsCmd) update(c *cli.Context) error { if err != nil { return err } + if pcConfig != nil { + if !common.IsOracleProvider(f.provider) { + warnUnsupportedProvisionedConcurrency() + } else if err := ApplyProvisionedConcurrency(f.provider, fn.ID, pcConfig); err != nil { + return err + } + } fmt.Println(appName, fnName, "updated") return nil @@ -529,19 +718,14 @@ func (f *fnsCmd) inspect(c *cli.Context) error { enc := json.NewEncoder(os.Stdout) enc.SetIndent("", "\t") - if prop == "" { - enc.Encode(fn) - return nil - } - - data, err := json.Marshal(fn) + inspect, err := buildInspectFnMap(fn) if err != nil { return fmt.Errorf("failed to inspect %s: %s", fnName, err) } - var inspect map[string]interface{} - err = json.Unmarshal(data, &inspect) - if err != nil { - return fmt.Errorf("failed to inspect %s: %s", fnName, err) + + if prop == "" { + enc.Encode(inspect) + return nil } jq := jsonq.NewQuery(inspect) diff --git a/objects/fn/fns_test.go b/objects/fn/fns_test.go new file mode 100644 index 00000000..4bb5cc3f --- /dev/null +++ b/objects/fn/fns_test.go @@ -0,0 +1,91 @@ +package fn + +import ( + "encoding/json" + "net/url" + "testing" + + "github.com/fnproject/cli/common" + models "github.com/fnproject/fn_go/modelsv2" + defaultprovider "github.com/fnproject/fn_go/provider/defaultprovider" +) + +func TestApplyProvisionedConcurrencyNoopForNilOrNonOracleProvider(t *testing.T) { + count := 40 + cfg := &common.OCIProvisionedConcurrencyConfig{Strategy: common.ProvisionedConcurrencyStrategyConstant, Count: &count} + + if err := ApplyProvisionedConcurrency(nil, "ocid1.fn.oc1..example", cfg); err != nil { + t.Fatalf("expected nil provider to be a no-op, got error %v", err) + } + + nonOracle := &defaultprovider.Provider{FnApiUrl: &url.URL{Scheme: "http", Host: "localhost:8080"}} + if err := ApplyProvisionedConcurrency(nonOracle, "ocid1.fn.oc1..example", cfg); err != nil { + t.Fatalf("expected non-oracle provider to be a no-op, got error %v", err) + } +} + +func TestFormatProvisionedConcurrencyDisplay(t *testing.T) { + count := 5 + fn := &models.Fn{Annotations: map[string]interface{}{ + annotationProvisionedConcurrencyStrategy: "CONSTANT", + annotationProvisionedConcurrencyCount: count, + }} + if got := formatProvisionedConcurrencyDisplay(fn); got != "constant:5" { + t.Fatalf("expected constant:5, got %q", got) + } + + fn = &models.Fn{Annotations: map[string]interface{}{ + annotationProvisionedConcurrencyStrategy: "NONE", + }} + if got := formatProvisionedConcurrencyDisplay(fn); got != "none" { + t.Fatalf("expected none, got %q", got) + } +} + +func TestBuildInspectFnMapIncludesProvisionedConcurrency(t *testing.T) { + count := 2 + fn := &models.Fn{ + Name: "hello", + Annotations: map[string]interface{}{ + annotationProvisionedConcurrencyStrategy: "CONSTANT", + annotationProvisionedConcurrencyCount: count, + }, + } + inspect, err := buildInspectFnMap(fn) + if err != nil { + t.Fatalf("buildInspectFnMap() error = %v", err) + } + pc, ok := inspect["provisionedConcurrency"] + if !ok { + t.Fatal("expected provisionedConcurrency field in inspect map") + } + data, err := json.Marshal(pc) + if err != nil { + t.Fatalf("failed to marshal provisionedConcurrency: %v", err) + } + var got map[string]interface{} + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("failed to unmarshal provisionedConcurrency: %v", err) + } + if got["strategy"] != "CONSTANT" { + t.Fatalf("expected strategy CONSTANT, got %#v", got["strategy"]) + } + if got["count"] != float64(2) { + t.Fatalf("expected count 2, got %#v", got["count"]) + } +} + +func TestSetProvisionedConcurrencyAnnotations(t *testing.T) { + count := 40 + fn := &models.Fn{} + cfg := &common.OCIProvisionedConcurrencyConfig{Strategy: common.ProvisionedConcurrencyStrategyConstant, Count: &count} + if err := SetProvisionedConcurrencyAnnotations(fn, cfg); err != nil { + t.Fatalf("SetProvisionedConcurrencyAnnotations() error = %v", err) + } + if fn.Annotations[annotationProvisionedConcurrencyStrategy] != common.ProvisionedConcurrencyStrategyConstant { + t.Fatalf("expected strategy annotation %q, got %#v", common.ProvisionedConcurrencyStrategyConstant, fn.Annotations[annotationProvisionedConcurrencyStrategy]) + } + if fn.Annotations[annotationProvisionedConcurrencyCount] != count { + t.Fatalf("expected count annotation %d, got %#v", count, fn.Annotations[annotationProvisionedConcurrencyCount]) + } +} \ No newline at end of file diff --git a/test/cli_init_test.go b/test/cli_init_test.go index 377ac64f..eedf1ef8 100644 --- a/test/cli_init_test.go +++ b/test/cli_init_test.go @@ -18,6 +18,7 @@ package test import ( "fmt" + "github.com/fnproject/cli/common" "github.com/fnproject/cli/testharness" "testing" ) @@ -147,3 +148,27 @@ func TestInitImage(t *testing.T) { }) } + +func TestInitProvisionedConcurrencyWritesYaml(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", "--runtime", "go", "--name", funcName, "--provisioned-concurrency", "constant:40", dirName).AssertSuccess() + + h.Cd(dirName) + yamlFile := h.GetYamlFile("func.yaml") + if yamlFile.Deploy == nil || yamlFile.Deploy.OCI == nil || yamlFile.Deploy.OCI.ProvisionedConcurrency == nil { + t.Fatal("expected provisioned concurrency settings in func.yaml") + } + if yamlFile.Deploy.OCI.ProvisionedConcurrency.Strategy != common.ProvisionedConcurrencyStrategyConstant { + t.Fatalf("expected strategy %q, got %q", common.ProvisionedConcurrencyStrategyConstant, yamlFile.Deploy.OCI.ProvisionedConcurrency.Strategy) + } + if yamlFile.Deploy.OCI.ProvisionedConcurrency.Count == nil || *yamlFile.Deploy.OCI.ProvisionedConcurrency.Count != 40 { + t.Fatalf("expected count 40, got %#v", yamlFile.Deploy.OCI.ProvisionedConcurrency.Count) + } +} diff --git a/vendor/github.com/fnproject/fn_go/provider/oracle/shim/client/client_mock.go b/vendor/github.com/fnproject/fn_go/provider/oracle/shim/client/client_mock.go index 930f09d0..c558210d 100644 --- a/vendor/github.com/fnproject/fn_go/provider/oracle/shim/client/client_mock.go +++ b/vendor/github.com/fnproject/fn_go/provider/oracle/shim/client/client_mock.go @@ -228,6 +228,7 @@ func NewMockFunctionsManagementClientBasic(ctrl *gomock.Controller) FunctionsMan digest := "GetFunctionDigest" memory := int64(128) timeout := 30 + pcCount := 5 invokeEndpoint := "GetFunctionInvokeEndpoint" return functions.GetFunctionResponse{ Function: functions.Function{ @@ -240,6 +241,7 @@ func NewMockFunctionsManagementClientBasic(ctrl *gomock.Controller) FunctionsMan ImageDigest: &digest, MemoryInMBs: &memory, TimeoutInSeconds: &timeout, + ProvisionedConcurrencyConfig: functions.ConstantProvisionedConcurrencyConfig{Count: &pcCount}, InvokeEndpoint: &invokeEndpoint, Config: map[string]string{ "GetFunctionKey1": "GetFunctionValue1", @@ -391,6 +393,7 @@ func newBasicFunctionSummary(n int, application *string) functions.FunctionSumma digest := "FunctionSummaryDigest" memory := int64(128) timeout := 30 + pcCount := n + 1 invokeEndpoint := "FunctionSummaryInvokeEndpoint" return functions.FunctionSummary{ Id: &id, @@ -402,6 +405,7 @@ func newBasicFunctionSummary(n int, application *string) functions.FunctionSumma ImageDigest: &digest, MemoryInMBs: &memory, TimeoutInSeconds: &timeout, + ProvisionedConcurrencyConfig: functions.ConstantProvisionedConcurrencyConfig{Count: &pcCount}, InvokeEndpoint: &invokeEndpoint, FreeformTags: nil, DefinedTags: nil, diff --git a/vendor/github.com/fnproject/fn_go/provider/oracle/shim/fns.go b/vendor/github.com/fnproject/fn_go/provider/oracle/shim/fns.go index 850475ec..3b8f724d 100644 --- a/vendor/github.com/fnproject/fn_go/provider/oracle/shim/fns.go +++ b/vendor/github.com/fnproject/fn_go/provider/oracle/shim/fns.go @@ -2,6 +2,7 @@ package shim import ( "fmt" + "strings" "github.com/fnproject/fn_go/clientv2/fns" "github.com/fnproject/fn_go/modelsv2" "github.com/fnproject/fn_go/provider/oracle/shim/client" @@ -15,6 +16,8 @@ const ( annotationImageDigest = "oracle.com/oci/imageDigest" annotationInvokeEndpoint = "fnproject.io/fn/invokeEndpoint" + annotationPCStrategy = "oracle.com/oci/provisionedConcurrencyStrategy" + annotationPCCount = "oracle.com/oci/provisionedConcurrencyCount" invokeEndpointFmtString = "%s/20181201/functions/%s/actions/invoke" ) @@ -40,12 +43,18 @@ func (s *fnsShim) CreateFn(params *fns.CreateFnParams) (*fns.CreateFnOK, error) return nil, err } + pcConfig, err := parseProvisionedConcurrencyAnnotation(params.Body.Annotations) + if err != nil { + return nil, err + } + details := functions.CreateFunctionDetails{ DisplayName: ¶ms.Body.Name, ApplicationId: ¶ms.Body.AppID, Image: ¶ms.Body.Image, MemoryInMBs: &memory, ImageDigest: digest, + ProvisionedConcurrencyConfig: pcConfig, Config: params.Body.Config, TimeoutInSeconds: parseTimeout(params.Body.Timeout), } @@ -237,6 +246,67 @@ func parseDigestAnnotation(annotations map[string]interface{}) (*string, error) return &digest, nil } +func parseProvisionedConcurrencyAnnotation(annotations map[string]interface{}) (functions.FunctionProvisionedConcurrencyConfig, error) { + if annotations == nil || len(annotations) == 0 { + return nil, nil + } + strategyRaw, ok := annotations[annotationPCStrategy] + if !ok { + return nil, nil + } + strategy, ok := strategyRaw.(string) + if !ok { + return nil, fmt.Errorf("invalid provisioned concurrency strategy") + } + switch strings.ToUpper(strings.TrimSpace(strategy)) { + case "NONE": + return functions.NoneProvisionedConcurrencyConfig{}, nil + case "CONSTANT": + countRaw, ok := annotations[annotationPCCount] + if !ok { + return nil, fmt.Errorf("invalid provisioned concurrency count") + } + var count int + switch typed := countRaw.(type) { + case int: + count = typed + case int32: + count = int(typed) + case int64: + count = int(typed) + case float64: + count = int(typed) + default: + return nil, fmt.Errorf("invalid provisioned concurrency count") + } + return functions.ConstantProvisionedConcurrencyConfig{Count: &count}, nil + default: + return nil, fmt.Errorf("invalid provisioned concurrency strategy") + } +} + +func addProvisionedConcurrencyAnnotations(annotations map[string]interface{}, cfg functions.FunctionProvisionedConcurrencyConfig) { + strategy := "NONE" + var count *int + + switch typed := cfg.(type) { + case functions.ConstantProvisionedConcurrencyConfig: + strategy = "CONSTANT" + count = typed.Count + case functions.NoneProvisionedConcurrencyConfig: + strategy = "NONE" + case nil: + strategy = "NONE" + default: + strategy = "NONE" + } + + annotations[annotationPCStrategy] = strategy + if count != nil { + annotations[annotationPCCount] = *count + } +} + func ociFnToV2(ociFn functions.Function) *modelsv2.Fn { annotations := make(map[string]interface{}) invokeEndpoint := fmt.Sprintf(invokeEndpointFmtString, *ociFn.InvokeEndpoint, *ociFn.Id) @@ -255,6 +325,7 @@ func ociFnToV2(ociFn functions.Function) *modelsv2.Fn { annotations[annotationImageDigest] = imageDigest annotations[annotationInvokeEndpoint] = invokeEndpoint + addProvisionedConcurrencyAnnotations(annotations, ociFn.ProvisionedConcurrencyConfig) var timeoutPtr *int32 if ociFn.TimeoutInSeconds != nil { @@ -295,6 +366,7 @@ func ociFnSummaryToV2(ociFnSummary functions.FunctionSummary) *modelsv2.Fn { annotations[annotationImageDigest] = imageDigest annotations[annotationInvokeEndpoint] = invokeEndpoint + addProvisionedConcurrencyAnnotations(annotations, ociFnSummary.ProvisionedConcurrencyConfig) var timeoutPtr *int32 if ociFnSummary.TimeoutInSeconds != nil {