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
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ all: dep build
build:
go build -o fn

generate-oci-parity:
@if [ -z "$(SPEC)" ]; then echo "SPEC is required. Usage: make generate-oci-parity SPEC=/absolute/path/to/functions-api-spec.yaml"; exit 1; fi
go run ./tools/oci_parity_gen --spec "$(SPEC)"

install:
go build -o ${GOPATH}/bin/fn

Expand Down
22 changes: 22 additions & 0 deletions commands/change_compartment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package commands

import (
"github.com/fnproject/cli/common"
"github.com/fnproject/cli/objects/app"
"github.com/urfave/cli"
)

func ChangeCompartmentCommand() cli.Command {
cmds := Cmd{
"apps": app.ChangeCompartment(),
}
return cli.Command{
Name: "change-compartment",
Usage: "\tMove a supported resource to another compartment",
Category: "MANAGEMENT COMMANDS",
ArgsUsage: "<subcommand>",
Description: "This command changes the compartment for supported OCI-backed resources.",
Subcommands: GetCommands(cmds),
BashComplete: common.DefaultBashComplete,
}
}
1 change: 1 addition & 0 deletions commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ var Commands = Cmd{
"build": BuildCommand(),
"build-server": BuildServerCommand(),
"bump": common.BumpCommand(),
"change-compartment": ChangeCompartmentCommand(),
"watch": WatchCommand(),
"invoke": InvokeCommand(),
"configure": ConfigureCommand(),
Expand Down
26 changes: 26 additions & 0 deletions common/json_input.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package common

import (
"encoding/json"
"fmt"
"os"
"strings"
)

func LoadCLIJSONInput(spec string, out interface{}) error {
trimmed := strings.TrimSpace(spec)
if trimmed == "" {
return nil
}
if strings.HasPrefix(trimmed, "file://") {
data, err := os.ReadFile(strings.TrimPrefix(trimmed, "file://"))
if err != nil {
return err
}
trimmed = string(data)
}
if err := json.Unmarshal([]byte(trimmed), out); err != nil {
return fmt.Errorf("invalid --from-json payload: %w", err)
}
return nil
}
33 changes: 33 additions & 0 deletions common/json_input_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package common

import (
"os"
"path/filepath"
"testing"
)

func TestLoadCLIJSONInput(t *testing.T) {
var parsed struct {
Name string `json:"name"`
}
if err := LoadCLIJSONInput(`{"name":"hello"}`, &parsed); err != nil {
t.Fatalf("LoadCLIJSONInput(raw) error = %v", err)
}
if parsed.Name != "hello" {
t.Fatalf("expected parsed name hello, got %q", parsed.Name)
}
tmp := t.TempDir()
path := filepath.Join(tmp, "input.json")
if err := os.WriteFile(path, []byte(`{"name":"world"}`), 0o644); err != nil {
t.Fatal(err)
}
parsed = struct {
Name string `json:"name"`
}{}
if err := LoadCLIJSONInput("file://"+path, &parsed); err != nil {
t.Fatalf("LoadCLIJSONInput(file) error = %v", err)
}
if parsed.Name != "world" {
t.Fatalf("expected parsed name world, got %q", parsed.Name)
}
}
144 changes: 144 additions & 0 deletions common/oci_request_control.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package common

import (
"context"
"fmt"
"os"
"strings"
"time"

"github.com/fnproject/fn_go/provider"
fnprovideroracle "github.com/fnproject/fn_go/provider/oracle"
ociCommon "github.com/oracle/oci-go-sdk/v65/common"
ocifunctions "github.com/oracle/oci-go-sdk/v65/functions"
"github.com/urfave/cli"
)

type OCIRequestControl struct {
IfMatch string
WaitForState string
MaxWaitSeconds int
WaitIntervalSeconds int
}

func ExtractOCIRequestControl(c *cli.Context) OCIRequestControl {
return OCIRequestControl{
IfMatch: strings.TrimSpace(c.String("if-match")),
WaitForState: strings.ToUpper(strings.TrimSpace(c.String("wait-for-state"))),
MaxWaitSeconds: c.Int("max-wait-seconds"),
WaitIntervalSeconds: c.Int("wait-interval-seconds"),
}
}

func (o OCIRequestControl) HasIfMatch() bool { return o.IfMatch != "" }
func (o OCIRequestControl) HasWait() bool { return o.WaitForState != "" }

func NormalizeWaitSettings(maxWait, interval int) (int, int) {
if maxWait <= 0 {
maxWait = 1200
}
if interval <= 0 {
interval = 5
}
return maxWait, interval
}

func WarnUnsupportedOCIRequestControl(p provider.Provider, control OCIRequestControl) {
if IsOracleProvider(p) {
return
}
if control.HasIfMatch() {
fmt.Fprintln(os.Stderr, "Warning: --if-match is only supported with an oracle provider and will be ignored.")
}
if control.HasWait() {
fmt.Fprintln(os.Stderr, "Warning: wait control flags are only supported with an oracle provider and will be ignored.")
}
}

func BuildOCIManagementClient(p provider.Provider) (*ocifunctions.FunctionsManagementClient, error) {
oracleProvider, ok := p.(*fnprovideroracle.OracleProvider)
if !ok || oracleProvider == nil {
return nil, nil
}
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
}

func waitUntil(deadline time.Time, interval time.Duration, check func() (bool, error)) error {
for {
done, err := check()
if err != nil {
return err
}
if done {
return nil
}
if time.Now().After(deadline) {
return fmt.Errorf("timed out waiting for requested state")
}
time.Sleep(interval)
}
}

func WaitForAppState(p provider.Provider, appID, targetState string, maxWaitSeconds, waitIntervalSeconds int) error {
if strings.TrimSpace(targetState) == "" || !IsOracleProvider(p) {
return nil
}
client, err := BuildOCIManagementClient(p)
if err != nil || client == nil {
return err
}
maxWaitSeconds, waitIntervalSeconds = NormalizeWaitSettings(maxWaitSeconds, waitIntervalSeconds)
deadline := time.Now().Add(time.Duration(maxWaitSeconds) * time.Second)
interval := time.Duration(waitIntervalSeconds) * time.Second
targetState = strings.ToUpper(strings.TrimSpace(targetState))
return waitUntil(deadline, interval, func() (bool, error) {
res, err := client.GetApplication(context.Background(), ocifunctions.GetApplicationRequest{ApplicationId: &appID})
if err != nil {
if targetState == "DELETED" {
if serr, ok := err.(ociCommon.ServiceError); ok && serr.GetHTTPStatusCode() == 404 {
return true, nil
}
}
return false, err
}
return strings.EqualFold(string(res.Application.LifecycleState), targetState), nil
})
}

func WaitForFunctionState(p provider.Provider, fnID, targetState string, maxWaitSeconds, waitIntervalSeconds int) error {
if strings.TrimSpace(targetState) == "" || !IsOracleProvider(p) {
return nil
}
client, err := BuildOCIManagementClient(p)
if err != nil || client == nil {
return err
}
maxWaitSeconds, waitIntervalSeconds = NormalizeWaitSettings(maxWaitSeconds, waitIntervalSeconds)
deadline := time.Now().Add(time.Duration(maxWaitSeconds) * time.Second)
interval := time.Duration(waitIntervalSeconds) * time.Second
targetState = strings.ToUpper(strings.TrimSpace(targetState))
return waitUntil(deadline, interval, func() (bool, error) {
res, err := client.GetFunction(context.Background(), ocifunctions.GetFunctionRequest{FunctionId: &fnID})
if err != nil {
if targetState == "DELETED" {
if serr, ok := err.(ociCommon.ServiceError); ok && serr.GetHTTPStatusCode() == 404 {
return true, nil
}
}
return false, err
}
return strings.EqualFold(string(res.Function.LifecycleState), targetState), nil
})
}
14 changes: 14 additions & 0 deletions common/oci_request_control_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package common

import "testing"

func TestNormalizeWaitSettings(t *testing.T) {
maxWait, interval := NormalizeWaitSettings(0, 0)
if maxWait != 1200 || interval != 5 {
t.Fatalf("unexpected defaults: maxWait=%d interval=%d", maxWait, interval)
}
maxWait, interval = NormalizeWaitSettings(30, 2)
if maxWait != 30 || interval != 2 {
t.Fatalf("expected explicit values to be preserved, got maxWait=%d interval=%d", maxWait, interval)
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/fnproject/cli
require (
github.com/coreos/go-semver v0.3.0
github.com/fatih/color v0.0.0-20170926111411-5df930a27be2
github.com/fnproject/fn_go v0.8.10
github.com/fnproject/fn_go v0.8.11
github.com/fsnotify/fsnotify v1.4.7
github.com/ghodss/yaml v1.0.0
github.com/giantswarm/semver-bump v0.0.0-20140912095342-88e6c9f2fe39
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/fatih/color v0.0.0-20170926111411-5df930a27be2 h1:40J76vs1Y7oiHFqTrQHQ6A5u8vbXJdLaMkC9iHU/uMw=
github.com/fatih/color v0.0.0-20170926111411-5df930a27be2/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fnproject/fn_go v0.8.10 h1:ETcdjVxfBSzRjdH4pS8xkaTAB8BrzYPjcuPbZFoYLfM=
github.com/fnproject/fn_go v0.8.10/go.mod h1:y8desXu8f+Y1oJDdNeb155tDwIn0MC9cWb6AU5D9XLs=
github.com/fnproject/fn_go v0.8.11 h1:BLDDMzlrPCbzp3O/AEkgRvxyezii2n2PGTf9S6oW450=
github.com/fnproject/fn_go v0.8.11/go.mod h1:BoSXYVGLW845/RUuiqOqPp5jNWRJjazakkuEYquQzsY=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
Expand Down
Loading