Skip to content

Commit 7cb2d8c

Browse files
committed
Add OCI Functions advanced parity support to Fn CLI
1 parent 3f01969 commit 7cb2d8c

39 files changed

Lines changed: 2672 additions & 84 deletions

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ all: dep build
44
build:
55
go build -o fn
66

7+
generate-oci-parity:
8+
@if [ -z "$(SPEC)" ]; then echo "SPEC is required. Usage: make generate-oci-parity SPEC=/absolute/path/to/functions-api-spec.yaml"; exit 1; fi
9+
go run ./tools/oci_parity_gen --spec "$(SPEC)"
10+
711
install:
812
go build -o ${GOPATH}/bin/fn
913

commands/change_compartment.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package commands
2+
3+
import (
4+
"github.com/fnproject/cli/common"
5+
"github.com/fnproject/cli/objects/app"
6+
"github.com/urfave/cli"
7+
)
8+
9+
func ChangeCompartmentCommand() cli.Command {
10+
cmds := Cmd{
11+
"apps": app.ChangeCompartment(),
12+
}
13+
return cli.Command{
14+
Name: "change-compartment",
15+
Usage: "\tMove a supported resource to another compartment",
16+
Category: "MANAGEMENT COMMANDS",
17+
ArgsUsage: "<subcommand>",
18+
Description: "This command changes the compartment for supported OCI-backed resources.",
19+
Subcommands: GetCommands(cmds),
20+
BashComplete: common.DefaultBashComplete,
21+
}
22+
}

commands/commands.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ var Commands = Cmd{
3737
"build": BuildCommand(),
3838
"build-server": BuildServerCommand(),
3939
"bump": common.BumpCommand(),
40+
"change-compartment": ChangeCompartmentCommand(),
4041
"watch": WatchCommand(),
4142
"invoke": InvokeCommand(),
4243
"configure": ConfigureCommand(),

common/json_input.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package common
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"strings"
8+
)
9+
10+
func LoadCLIJSONInput(spec string, out interface{}) error {
11+
trimmed := strings.TrimSpace(spec)
12+
if trimmed == "" {
13+
return nil
14+
}
15+
if strings.HasPrefix(trimmed, "file://") {
16+
data, err := os.ReadFile(strings.TrimPrefix(trimmed, "file://"))
17+
if err != nil {
18+
return err
19+
}
20+
trimmed = string(data)
21+
}
22+
if err := json.Unmarshal([]byte(trimmed), out); err != nil {
23+
return fmt.Errorf("invalid --from-json payload: %w", err)
24+
}
25+
return nil
26+
}

common/json_input_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package common
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
)
8+
9+
func TestLoadCLIJSONInput(t *testing.T) {
10+
var parsed struct {
11+
Name string `json:"name"`
12+
}
13+
if err := LoadCLIJSONInput(`{"name":"hello"}`, &parsed); err != nil {
14+
t.Fatalf("LoadCLIJSONInput(raw) error = %v", err)
15+
}
16+
if parsed.Name != "hello" {
17+
t.Fatalf("expected parsed name hello, got %q", parsed.Name)
18+
}
19+
tmp := t.TempDir()
20+
path := filepath.Join(tmp, "input.json")
21+
if err := os.WriteFile(path, []byte(`{"name":"world"}`), 0o644); err != nil {
22+
t.Fatal(err)
23+
}
24+
parsed = struct {
25+
Name string `json:"name"`
26+
}{}
27+
if err := LoadCLIJSONInput("file://"+path, &parsed); err != nil {
28+
t.Fatalf("LoadCLIJSONInput(file) error = %v", err)
29+
}
30+
if parsed.Name != "world" {
31+
t.Fatalf("expected parsed name world, got %q", parsed.Name)
32+
}
33+
}

common/oci_request_control.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package common
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"strings"
8+
"time"
9+
10+
"github.com/fnproject/fn_go/provider"
11+
fnprovideroracle "github.com/fnproject/fn_go/provider/oracle"
12+
ociCommon "github.com/oracle/oci-go-sdk/v65/common"
13+
ocifunctions "github.com/oracle/oci-go-sdk/v65/functions"
14+
"github.com/urfave/cli"
15+
)
16+
17+
type OCIRequestControl struct {
18+
IfMatch string
19+
WaitForState string
20+
MaxWaitSeconds int
21+
WaitIntervalSeconds int
22+
}
23+
24+
func ExtractOCIRequestControl(c *cli.Context) OCIRequestControl {
25+
return OCIRequestControl{
26+
IfMatch: strings.TrimSpace(c.String("if-match")),
27+
WaitForState: strings.ToUpper(strings.TrimSpace(c.String("wait-for-state"))),
28+
MaxWaitSeconds: c.Int("max-wait-seconds"),
29+
WaitIntervalSeconds: c.Int("wait-interval-seconds"),
30+
}
31+
}
32+
33+
func (o OCIRequestControl) HasIfMatch() bool { return o.IfMatch != "" }
34+
func (o OCIRequestControl) HasWait() bool { return o.WaitForState != "" }
35+
36+
func NormalizeWaitSettings(maxWait, interval int) (int, int) {
37+
if maxWait <= 0 {
38+
maxWait = 1200
39+
}
40+
if interval <= 0 {
41+
interval = 5
42+
}
43+
return maxWait, interval
44+
}
45+
46+
func WarnUnsupportedOCIRequestControl(p provider.Provider, control OCIRequestControl) {
47+
if IsOracleProvider(p) {
48+
return
49+
}
50+
if control.HasIfMatch() {
51+
fmt.Fprintln(os.Stderr, "Warning: --if-match is only supported with an oracle provider and will be ignored.")
52+
}
53+
if control.HasWait() {
54+
fmt.Fprintln(os.Stderr, "Warning: wait control flags are only supported with an oracle provider and will be ignored.")
55+
}
56+
}
57+
58+
func BuildOCIManagementClient(p provider.Provider) (*ocifunctions.FunctionsManagementClient, error) {
59+
oracleProvider, ok := p.(*fnprovideroracle.OracleProvider)
60+
if !ok || oracleProvider == nil {
61+
return nil, nil
62+
}
63+
client, err := ocifunctions.NewFunctionsManagementClientWithConfigurationProvider(oracleProvider.ConfigurationProvider)
64+
if err != nil {
65+
return nil, err
66+
}
67+
if oracleProvider.FnApiUrl != nil {
68+
client.Host = oracleProvider.FnApiUrl.String()
69+
} else {
70+
region, _ := oracleProvider.ConfigurationProvider.Region()
71+
if region != "" {
72+
client.SetRegion(region)
73+
}
74+
}
75+
return &client, nil
76+
}
77+
78+
func waitUntil(deadline time.Time, interval time.Duration, check func() (bool, error)) error {
79+
for {
80+
done, err := check()
81+
if err != nil {
82+
return err
83+
}
84+
if done {
85+
return nil
86+
}
87+
if time.Now().After(deadline) {
88+
return fmt.Errorf("timed out waiting for requested state")
89+
}
90+
time.Sleep(interval)
91+
}
92+
}
93+
94+
func WaitForAppState(p provider.Provider, appID, targetState string, maxWaitSeconds, waitIntervalSeconds int) error {
95+
if strings.TrimSpace(targetState) == "" || !IsOracleProvider(p) {
96+
return nil
97+
}
98+
client, err := BuildOCIManagementClient(p)
99+
if err != nil || client == nil {
100+
return err
101+
}
102+
maxWaitSeconds, waitIntervalSeconds = NormalizeWaitSettings(maxWaitSeconds, waitIntervalSeconds)
103+
deadline := time.Now().Add(time.Duration(maxWaitSeconds) * time.Second)
104+
interval := time.Duration(waitIntervalSeconds) * time.Second
105+
targetState = strings.ToUpper(strings.TrimSpace(targetState))
106+
return waitUntil(deadline, interval, func() (bool, error) {
107+
res, err := client.GetApplication(context.Background(), ocifunctions.GetApplicationRequest{ApplicationId: &appID})
108+
if err != nil {
109+
if targetState == "DELETED" {
110+
if serr, ok := err.(ociCommon.ServiceError); ok && serr.GetHTTPStatusCode() == 404 {
111+
return true, nil
112+
}
113+
}
114+
return false, err
115+
}
116+
return strings.EqualFold(string(res.Application.LifecycleState), targetState), nil
117+
})
118+
}
119+
120+
func WaitForFunctionState(p provider.Provider, fnID, targetState string, maxWaitSeconds, waitIntervalSeconds int) error {
121+
if strings.TrimSpace(targetState) == "" || !IsOracleProvider(p) {
122+
return nil
123+
}
124+
client, err := BuildOCIManagementClient(p)
125+
if err != nil || client == nil {
126+
return err
127+
}
128+
maxWaitSeconds, waitIntervalSeconds = NormalizeWaitSettings(maxWaitSeconds, waitIntervalSeconds)
129+
deadline := time.Now().Add(time.Duration(maxWaitSeconds) * time.Second)
130+
interval := time.Duration(waitIntervalSeconds) * time.Second
131+
targetState = strings.ToUpper(strings.TrimSpace(targetState))
132+
return waitUntil(deadline, interval, func() (bool, error) {
133+
res, err := client.GetFunction(context.Background(), ocifunctions.GetFunctionRequest{FunctionId: &fnID})
134+
if err != nil {
135+
if targetState == "DELETED" {
136+
if serr, ok := err.(ociCommon.ServiceError); ok && serr.GetHTTPStatusCode() == 404 {
137+
return true, nil
138+
}
139+
}
140+
return false, err
141+
}
142+
return strings.EqualFold(string(res.Function.LifecycleState), targetState), nil
143+
})
144+
}

common/oci_request_control_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package common
2+
3+
import "testing"
4+
5+
func TestNormalizeWaitSettings(t *testing.T) {
6+
maxWait, interval := NormalizeWaitSettings(0, 0)
7+
if maxWait != 1200 || interval != 5 {
8+
t.Fatalf("unexpected defaults: maxWait=%d interval=%d", maxWait, interval)
9+
}
10+
maxWait, interval = NormalizeWaitSettings(30, 2)
11+
if maxWait != 30 || interval != 2 {
12+
t.Fatalf("expected explicit values to be preserved, got maxWait=%d interval=%d", maxWait, interval)
13+
}
14+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/fnproject/cli
33
require (
44
github.com/coreos/go-semver v0.3.0
55
github.com/fatih/color v0.0.0-20170926111411-5df930a27be2
6-
github.com/fnproject/fn_go v0.8.10
6+
github.com/fnproject/fn_go v0.8.11
77
github.com/fsnotify/fsnotify v1.4.7
88
github.com/ghodss/yaml v1.0.0
99
github.com/giantswarm/semver-bump v0.0.0-20140912095342-88e6c9f2fe39

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
3939
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
4040
github.com/fatih/color v0.0.0-20170926111411-5df930a27be2 h1:40J76vs1Y7oiHFqTrQHQ6A5u8vbXJdLaMkC9iHU/uMw=
4141
github.com/fatih/color v0.0.0-20170926111411-5df930a27be2/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
42-
github.com/fnproject/fn_go v0.8.10 h1:ETcdjVxfBSzRjdH4pS8xkaTAB8BrzYPjcuPbZFoYLfM=
43-
github.com/fnproject/fn_go v0.8.10/go.mod h1:y8desXu8f+Y1oJDdNeb155tDwIn0MC9cWb6AU5D9XLs=
42+
github.com/fnproject/fn_go v0.8.11 h1:BLDDMzlrPCbzp3O/AEkgRvxyezii2n2PGTf9S6oW450=
43+
github.com/fnproject/fn_go v0.8.11/go.mod h1:BoSXYVGLW845/RUuiqOqPp5jNWRJjazakkuEYquQzsY=
4444
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
4545
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
4646
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=

0 commit comments

Comments
 (0)