Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions commands/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ func DeployCommand() cli.Command {
if err != nil {
return err
}
cmd.provider = provider
cmd.clientV2 = provider.APIClientv2()
return nil
},
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
9 changes: 9 additions & 0 deletions commands/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:<count>'",
},
}

return fgs
Expand Down Expand Up @@ -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")
Expand Down
84 changes: 84 additions & 0 deletions common/provisioned_concurrency.go
Original file line number Diff line number Diff line change
@@ -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:<count>
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:<count>')", 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
}
54 changes: 54 additions & 0 deletions common/provisioned_concurrency_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
})
}
}
Loading