diff --git a/Makefile b/Makefile index ff394bc9..5185503e 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index e5ab72b6..60d3cd54 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,42 @@ deploy: When these OCI-specific flags are used with a non-Oracle provider or local Fn server workflows, Fn CLI accepts them and emits user-friendly warnings where the settings are not applicable. -### Pre-Built Function create support +### Pre-Built Function (PBF) support +List available Pre-Built Functions: + +```sh +fn list pbfs +fn list pbfs --search Document +fn list pbfs --trigger http +fn list pbfs --output json +``` + +Get a specific PBF listing: + +```sh +fn get pbfs +``` + +List versions for a PBF: + +```sh +fn list pbfs versions +fn list pbfs versions --latest +``` + +Get a specific PBF version: + +```sh +fn get pbfs version +``` + +List supported PBF trigger names: + +```sh +fn list pbfs triggers +fn list pbfs triggers http +``` + Create a function from a Pre-Built Function (PBF) listing OCID: ```sh @@ -164,7 +199,42 @@ For PBF create flows: - Fn CLI automatically resolves the minimum required memory from the current PBF version when possible - if you specify `--memory`, it must be greater than or equal to the PBF minimum requirement -Current limitation: `--pbf` support is currently focused on `fn create function` and is not yet persisted through `func.yaml` / deploy flows. +Persist a PBF-backed function definition using `fn init`: + +```sh +fn init --name hello-pbf --pbf +``` + +Example `func.yaml` for a PBF-backed function: + +```yaml +deploy: + oci: + pbf: + listing_id: +``` + +Deploy a persisted PBF-backed function: + +```sh +fn deploy --app +``` + +For PBF-backed deploys, Fn CLI skips image build/push/sign flows and creates or updates the function using PBF source details instead. + +Inspect/list output also surfaces PBF-backed functions: + +```sh +fn inspect function +fn list functions +``` + +`fn list functions` includes a `SOURCE` column, for example: + +```text +NAME IMAGE SOURCE ID +hello-pbf pbf: +``` ### Detached invoke examples Invoke a function in detached mode: diff --git a/commands/change_compartment.go b/commands/change_compartment.go new file mode 100644 index 00000000..11a38396 --- /dev/null +++ b/commands/change_compartment.go @@ -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: "", + Description: "This command changes the compartment for supported OCI-backed resources.", + Subcommands: GetCommands(cmds), + BashComplete: common.DefaultBashComplete, + } +} \ No newline at end of file diff --git a/commands/commands.go b/commands/commands.go index 29bb2641..041a0b9a 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -23,6 +23,7 @@ import ( "github.com/fnproject/cli/objects/app" "github.com/fnproject/cli/objects/context" "github.com/fnproject/cli/objects/fn" + "github.com/fnproject/cli/objects/pbf" "github.com/fnproject/cli/objects/server" "github.com/fnproject/cli/objects/trigger" "github.com/urfave/cli" @@ -36,6 +37,7 @@ var Commands = Cmd{ "build": BuildCommand(), "build-server": BuildServerCommand(), "bump": common.BumpCommand(), + "change-compartment": ChangeCompartmentCommand(), "watch": WatchCommand(), "invoke": InvokeCommand(), "configure": ConfigureCommand(), @@ -97,6 +99,7 @@ var DeleteCmds = Cmd{ var GetCmds = Cmd{ "config": ConfigCommand("get"), + "pbfs": pbf.Get(), } var InspectCmds = Cmd{ @@ -110,6 +113,7 @@ var ListCmds = Cmd{ "config": ConfigCommand("list"), "apps": app.List(), "functions": fn.List(), + "pbfs": pbf.List(), "triggers": trigger.List(), "contexts": context.List(), } diff --git a/commands/deploy.go b/commands/deploy.go index 9e2b176d..c8cf0e06 100644 --- a/commands/deploy.go +++ b/commands/deploy.go @@ -355,7 +355,8 @@ func (p *deploycmd) deployFuncV20180708(c *cli.Context, app *models.App, funcfil common.WarnIfOCIManagedFunctionSettingsUnsupported(os.Stderr, p.provider, funcfile.Name, funcfile) oracleProvider, _ := getOracleProvider() - if oracleProvider != nil && oracleProvider.ImageCompartmentID != "" { + isPBFDeploy := funcfile.Deploy != nil && funcfile.Deploy.OCI != nil && funcfile.Deploy.OCI.PBF != nil && strings.TrimSpace(funcfile.Deploy.OCI.PBF.ListingID) != "" + if !isPBFDeploy && oracleProvider != nil && oracleProvider.ImageCompartmentID != "" { // If the provider is Oracle and ImageCompartmentID is present, we need to deploy image to the ImageCompartmentID. // The repository name should be unique throughout a tenancy. We check if a repository exists in the compartment and create it if it doesn't already exist. // If the creation fails, it could be because the repository name aready exists in a different compartment. @@ -393,36 +394,42 @@ func (p *deploycmd) deployFuncV20180708(c *cli.Context, app *models.App, funcfil // TODO: this whole funcfile handling needs some love, way too confusing. Only bump makes permanent changes to it. } - buildArgs := c.StringSlice("build-arg") + if !isPBFDeploy { + buildArgs := c.StringSlice("build-arg") - // In case of local ignore the architectures parameter - shape := "" - if !p.local && !p.localDebug { - // fetch the architectures - shape = app.Shape - if shape == "" { - shape = common.DefaultAppShape - app.Shape = shape - } + // In case of local ignore the architectures parameter + shape := "" + if !p.local && !p.localDebug { + // fetch the architectures + shape = app.Shape + if shape == "" { + shape = common.DefaultAppShape + app.Shape = shape + } - if _, ok := common.ShapeMap[shape]; !ok { - return errors.New(fmt.Sprintf("Invalid application : %s shape: %s", app.Name, shape)) + if _, ok := common.ShapeMap[shape]; !ok { + return errors.New(fmt.Sprintf("Invalid application : %s shape: %s", app.Name, shape)) + } } - } - _, err := common.BuildFuncV20180708(common.IsVerbose(), funcfilePath, funcfile, buildArgs, p.noCache, shape, p.localDebug) - if err != nil { - return err - } + _, err := common.BuildFuncV20180708(common.IsVerbose(), funcfilePath, funcfile, buildArgs, p.noCache, shape, p.localDebug) + if err != nil { + return err + } - if err := p.signImage(funcfile); err != nil { - return err + if err := p.signImage(funcfile); err != nil { + return err + } } return p.updateFunction(c, app.ID, funcfile) } 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.PBF != nil && strings.TrimSpace(ff.Deploy.OCI.PBF.ListingID) != "" { + fmt.Printf("Updating function %s using PBF listing %s...\n", ff.Name, ff.Deploy.OCI.PBF.ListingID) + } else { + fmt.Printf("Updating function %s using image %s...\n", ff.Name, ff.ImageNameV20180708()) + } var detachedSeconds int if ff.Deploy != nil && ff.Deploy.OCI != nil && ff.Deploy.OCI.DetachedMode != nil && ff.Deploy.OCI.DetachedMode.Timeout != "" { _, seconds, err := common.ParseDetachedTimeoutSpec(ff.Deploy.OCI.DetachedMode.Timeout) @@ -441,6 +448,11 @@ func (p *deploycmd) updateFunction(c *cli.Context, appID string, ff *common.Func if err := function.WithFuncFileV20180708(ff, fn); err != nil { return fmt.Errorf("Error getting function with funcfile: %s", err) } + if ff.Deploy != nil && ff.Deploy.OCI != nil && ff.Deploy.OCI.PBF != nil { + if err := function.ResolvePBFMemoryForListing(p.provider, fn, ff.Deploy.OCI.PBF.ListingID); err != nil { + return err + } + } if detachedSeconds > 0 && common.IsOracleProvider(p.provider) { function.SetDetachedTimeoutAnnotation(fn, detachedSeconds) } diff --git a/commands/init.go b/commands/init.go index 776a7eca..0c90eec5 100644 --- a/commands/init.go +++ b/commands/init.go @@ -143,6 +143,10 @@ func initFlags(a *initFnCmd) []cli.Flag { Name: "provisioned-concurrency", Usage: "Set OCI provisioned concurrency using 'none' or 'constant:'", }, + cli.StringFlag{ + Name: "pbf", + Usage: "Initialize func.yaml for a Pre-Built Function using a PBF listing OCID", + }, } return fgs @@ -220,6 +224,15 @@ func (a *initFnCmd) init(c *cli.Context) error { return err } common.SetProvisionedConcurrency(a.ff, pcConfig) + if pbfListingID := strings.TrimSpace(c.String("pbf")); pbfListingID != "" { + if a.ff.Deploy == nil { + a.ff.Deploy = &common.FuncDeployConfig{} + } + if a.ff.Deploy.OCI == nil { + a.ff.Deploy.OCI = &common.OCIFunctionDeployConfig{} + } + a.ff.Deploy.OCI.PBF = &common.OCIPBFSourceConfig{ListingID: pbfListingID} + } freeformTags, err := common.ParseFreeformTagSpecs(c.StringSlice("tag")) if err != nil { return err @@ -249,7 +262,6 @@ func (a *initFnCmd) init(c *cli.Context) error { return fmt.Errorf("Runtime %s is no more supported for new apps. Please use python or %s runtime for new apps.", runtime, runtime[:strings.LastIndex(runtime, ".")]) } } - path := c.Args().First() if path != "" { fmt.Printf("Creating function at: ./%s\n", path) @@ -427,6 +439,9 @@ func (a *initFnCmd) BuildFuncFileV20180708(c *cli.Context, path string) error { if c.String("init-image") != "" { return nil } + if strings.TrimSpace(c.String("pbf")) != "" { + return nil + } var helper langs.LangHelper if runtime == "" { diff --git a/common/funcfile.go b/common/funcfile.go index c7409517..39581135 100644 --- a/common/funcfile.go +++ b/common/funcfile.go @@ -87,10 +87,16 @@ type OCIProvisionedConcurrencyConfig struct { Count *int `yaml:"count,omitempty" json:"count,omitempty"` } +// OCIPBFSourceConfig stores Pre-Built Function source details for OCI Functions. +type OCIPBFSourceConfig struct { + ListingID string `yaml:"listing_id,omitempty" json:"listing_id,omitempty"` +} + // OCIFunctionDeployConfig stores OCI-specific deploy configuration for a function. type OCIFunctionDeployConfig struct { ProvisionedConcurrency *OCIProvisionedConcurrencyConfig `yaml:"provisioned_concurrency,omitempty" json:"provisioned_concurrency,omitempty"` DetachedMode *OCIDetachedModeConfig `yaml:"detached_mode,omitempty" json:"detached_mode,omitempty"` + PBF *OCIPBFSourceConfig `yaml:"pbf,omitempty" json:"pbf,omitempty"` FreeformTags map[string]string `yaml:"freeform_tags,omitempty" json:"freeform_tags,omitempty"` DefinedTags OCIDefinedTags `yaml:"defined_tags,omitempty" json:"defined_tags,omitempty"` } @@ -439,6 +445,9 @@ func (ff *FuncFileV20180708) HasOCIManagedFunctionSettings() bool { if len(oci.FreeformTags) > 0 || len(oci.DefinedTags) > 0 { return true } + if oci.PBF != nil && strings.TrimSpace(oci.PBF.ListingID) != "" { + return true + } return false } @@ -459,6 +468,9 @@ func (ff *FuncFileV20180708) OCIManagedFunctionSettingNames() []string { settings = append(settings, "detached_mode") } settings = append(settings, OCIManagedResourceTagSettingNames(ff)...) + if oci.PBF != nil && strings.TrimSpace(oci.PBF.ListingID) != "" { + settings = append(settings, "pbf") + } return settings } diff --git a/common/json_input.go b/common/json_input.go new file mode 100644 index 00000000..949820b8 --- /dev/null +++ b/common/json_input.go @@ -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 +} diff --git a/common/json_input_test.go b/common/json_input_test.go new file mode 100644 index 00000000..4bbb8baa --- /dev/null +++ b/common/json_input_test.go @@ -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) + } +} diff --git a/common/oci_request_control.go b/common/oci_request_control.go new file mode 100644 index 00000000..6cdc79e5 --- /dev/null +++ b/common/oci_request_control.go @@ -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 + }) +} diff --git a/common/oci_request_control_test.go b/common/oci_request_control_test.go new file mode 100644 index 00000000..72965944 --- /dev/null +++ b/common/oci_request_control_test.go @@ -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) + } +} diff --git a/go.mod b/go.mod index 128a5ac5..5463b2e5 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 0ad6f06b..b78267d5 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/ociparity/generator.go b/internal/ociparity/generator.go new file mode 100644 index 00000000..a7735b35 --- /dev/null +++ b/internal/ociparity/generator.go @@ -0,0 +1,221 @@ +package ociparity + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strings" +) + +type GeneratedField struct { + FlagName string + Property string + Description string + Kind string +} + +type AppGenerationModel struct { + CreateUpdateFields []GeneratedField + ListFields []GeneratedField +} + +type FnGenerationModel struct { + CreateUpdateFields []GeneratedField + ListFields []GeneratedField +} + +func BuildAppModel(spec *Spec) (*AppGenerationModel, error) { + createSchema, err := spec.Schema("CreateApplicationDetails") + if err != nil { + return nil, err + } + updateSchema, err := spec.Schema("UpdateApplicationDetails") + if err != nil { + return nil, err + } + createSchema, err = ParseObjectOrRef(spec, createSchema) + if err != nil { + return nil, err + } + updateSchema, err = ParseObjectOrRef(spec, updateSchema) + if err != nil { + return nil, err + } + fields := map[string]GeneratedField{} + collect := func(schema map[string]interface{}) { + props, _ := schema["properties"].(map[string]interface{}) + for _, propName := range []string{"traceConfig", "networkSecurityGroupIds", "imagePolicyConfig", "securityAttributes"} { + if propNode, ok := props[propName].(map[string]interface{}); ok { + kind := "string" + switch propName { + case "traceConfig", "imagePolicyConfig", "securityAttributes": + kind = "json" + case "networkSecurityGroupIds": + kind = "string-slice" + } + flagName := toFlagName(propName) + fields[propName] = GeneratedField{ + FlagName: flagName, + Property: propName, + Description: normalizeDescription(propNode["description"]), + Kind: kind, + } + } + } + } + collect(createSchema) + collect(updateSchema) + ordered := make([]GeneratedField, 0, len(fields)) + for _, f := range fields { + ordered = append(ordered, f) + } + sort.Slice(ordered, func(i, j int) bool { return ordered[i].FlagName < ordered[j].FlagName }) + listFields := []GeneratedField{ + {FlagName: "display-name", Property: "displayName", Description: "Filter applications by exact display name", Kind: "string"}, + {FlagName: "id", Property: "id", Description: "Filter applications by OCID", Kind: "string"}, + {FlagName: "lifecycle-state", Property: "lifecycleState", Description: "Filter applications by lifecycle state", Kind: "string"}, + {FlagName: "sort-by", Property: "sortBy", Description: "Sort applications by a supported field", Kind: "string"}, + {FlagName: "sort-order", Property: "sortOrder", Description: "Sort order for list results", Kind: "string"}, + } + return &AppGenerationModel{CreateUpdateFields: ordered, ListFields: listFields}, nil +} + +func toFlagName(property string) string { + var out []rune + for i, r := range property { + if i > 0 && r >= 'A' && r <= 'Z' { + out = append(out, '-') + } + if r >= 'A' && r <= 'Z' { + r = r + ('a' - 'A') + } + out = append(out, r) + } + return string(out) +} + +func GenerateAppFiles(specPath string) (map[string]string, error) { + spec, err := LoadSpec(specPath) + if err != nil { + return nil, err + } + model, err := BuildAppModel(spec) + if err != nil { + return nil, err + } + return map[string]string{ + filepath.ToSlash("objects/app/generated_oci_parity_flags.go"): renderAppFlags(model), + filepath.ToSlash("objects/app/generated_oci_parity_apply.go"): renderAppApply(model), + filepath.ToSlash("objects/app/generated_oci_parity_list.go"): renderAppList(model), + filepath.ToSlash("vendor/github.com/fnproject/fn_go/provider/oracle/shim/generated_app_oci_parity.go"): renderAppShim(model), + }, nil +} + +func BuildFnModel(spec *Spec) (*FnGenerationModel, error) { + createSchema, err := spec.Schema("CreateFunctionDetails") + if err != nil { + return nil, err + } + updateSchema, err := spec.Schema("UpdateFunctionDetails") + if err != nil { + return nil, err + } + createSchema, err = ParseObjectOrRef(spec, createSchema) + if err != nil { + return nil, err + } + updateSchema, err = ParseObjectOrRef(spec, updateSchema) + if err != nil { + return nil, err + } + fields := map[string]GeneratedField{} + collect := func(schema map[string]interface{}) { + props, _ := schema["properties"].(map[string]interface{}) + for _, propName := range []string{"traceConfig", "timeoutInSeconds", "detachedModeTimeoutInSeconds", "successDestination", "failureDestination"} { + if propNode, ok := props[propName].(map[string]interface{}); ok { + kind := "string" + switch propName { + case "traceConfig", "successDestination", "failureDestination": + kind = "json" + case "timeoutInSeconds", "detachedModeTimeoutInSeconds": + kind = "int" + } + flagName := toFlagName(propName) + fields[propName] = GeneratedField{ + FlagName: flagName, + Property: propName, + Description: normalizeDescription(propNode["description"]), + Kind: kind, + } + } + } + } + collect(createSchema) + collect(updateSchema) + ordered := make([]GeneratedField, 0, len(fields)) + for _, f := range fields { + ordered = append(ordered, f) + } + sort.Slice(ordered, func(i, j int) bool { return ordered[i].FlagName < ordered[j].FlagName }) + listFields := []GeneratedField{ + {FlagName: "display-name", Property: "displayName", Description: "Filter functions by exact display name", Kind: "string"}, + {FlagName: "id", Property: "id", Description: "Filter functions by OCID", Kind: "string"}, + {FlagName: "lifecycle-state", Property: "lifecycleState", Description: "Filter functions by lifecycle state", Kind: "string"}, + {FlagName: "sort-by", Property: "sortBy", Description: "Sort functions by a supported field", Kind: "string"}, + {FlagName: "sort-order", Property: "sortOrder", Description: "Sort order for list results", Kind: "string"}, + } + return &FnGenerationModel{CreateUpdateFields: ordered, ListFields: listFields}, nil +} + +func GenerateFnFiles(specPath string) (map[string]string, error) { + spec, err := LoadSpec(specPath) + if err != nil { + return nil, err + } + model, err := BuildFnModel(spec) + if err != nil { + return nil, err + } + return map[string]string{ + filepath.ToSlash("objects/fn/generated_oci_parity_flags.go"): renderFnFlags(model), + filepath.ToSlash("objects/fn/generated_oci_parity_apply.go"): renderFnApply(model), + filepath.ToSlash("objects/fn/generated_oci_parity_list.go"): renderFnList(model), + filepath.ToSlash("vendor/github.com/fnproject/fn_go/provider/oracle/shim/generated_fn_oci_parity.go"): renderFnShim(model), + }, nil +} + +func WriteGeneratedFiles(root, specPath string) error { + files, err := GenerateAppFiles(specPath) + if err != nil { + return err + } + fnFiles, err := GenerateFnFiles(specPath) + if err != nil { + return err + } + for k, v := range fnFiles { + files[k] = v + } + for rel, content := range files { + path := filepath.Join(root, filepath.FromSlash(rel)) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + return err + } + } + return nil +} + +func normalizeDescription(value interface{}) string { + if value == nil { + return "" + } + text := strings.TrimSpace(fmt.Sprint(value)) + if text == "" { + return "" + } + return text +} diff --git a/internal/ociparity/generator_test.go b/internal/ociparity/generator_test.go new file mode 100644 index 00000000..44f1a85a --- /dev/null +++ b/internal/ociparity/generator_test.go @@ -0,0 +1,170 @@ +package ociparity + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestGenerateAppFiles(t *testing.T) { + tmp := t.TempDir() + specPath := filepath.Join(tmp, "spec.yaml") + spec := `openapi: 3.0.0 +components: + schemas: + CreateApplicationDetails: + type: object + properties: + traceConfig: + type: object + description: app trace config + networkSecurityGroupIds: + type: array + items: + type: string + description: NSG ids + UpdateApplicationDetails: + type: object + properties: + traceConfig: + type: object + networkSecurityGroupIds: + type: array + items: + type: string +` + if err := os.WriteFile(specPath, []byte(spec), 0o644); err != nil { + t.Fatal(err) + } + files, err := GenerateAppFiles(specPath) + if err != nil { + t.Fatalf("GenerateAppFiles() error = %v", err) + } + flags := files[filepath.ToSlash("objects/app/generated_oci_parity_flags.go")] + if !strings.Contains(flags, "trace-config") || !strings.Contains(flags, "network-security-group-ids") { + t.Fatalf("generated flags missing expected fields:\n%s", flags) + } + apply := files[filepath.ToSlash("objects/app/generated_oci_parity_apply.go")] + if !strings.Contains(apply, "ApplyGeneratedOCIParityAppFlags") { + t.Fatalf("generated apply helper missing:\n%s", apply) + } + list := files[filepath.ToSlash("objects/app/generated_oci_parity_list.go")] + if !strings.Contains(list, "ApplyGeneratedOCIParityAppListParams") || !strings.Contains(list, "params.SortOrder") { + t.Fatalf("generated list helper missing expected content:\n%s", list) + } + shim := files[filepath.ToSlash("vendor/github.com/fnproject/fn_go/provider/oracle/shim/generated_app_oci_parity.go")] + if !strings.Contains(shim, "applyGeneratedOCIParityCreateApplicationDetails") || !strings.Contains(shim, "applyGeneratedOCIParityListApplicationsRequest") { + t.Fatalf("generated shim helper missing:\n%s", shim) + } +} + +func TestGenerateAppFilesSwagger2Definitions(t *testing.T) { + tmp := t.TempDir() + specPath := filepath.Join(tmp, "spec.yaml") + spec := `swagger: '2.0' +definitions: + CreateApplicationDetails: + type: object + properties: + traceConfig: + type: object + networkSecurityGroupIds: + type: array + items: + type: string + UpdateApplicationDetails: + type: object + properties: + traceConfig: + type: object + networkSecurityGroupIds: + type: array + items: + type: string +` + if err := os.WriteFile(specPath, []byte(spec), 0o644); err != nil { + t.Fatal(err) + } + files, err := GenerateAppFiles(specPath) + if err != nil { + t.Fatalf("GenerateAppFiles() error = %v", err) + } + flags := files[filepath.ToSlash("objects/app/generated_oci_parity_flags.go")] + if !strings.Contains(flags, "trace-config") || !strings.Contains(flags, "network-security-group-ids") { + t.Fatalf("generated flags missing expected app fields:\n%s", flags) + } + if !strings.Contains(flags, "lifecycle-state") || !strings.Contains(flags, "sort-order") || !strings.Contains(flags, "display-name") { + t.Fatalf("generated list flags missing expected app fields:\n%s", flags) + } +} + +func TestGenerateFnFilesSwagger2Definitions(t *testing.T) { + tmp := t.TempDir() + specPath := filepath.Join(tmp, "spec.yaml") + spec := `swagger: '2.0' +definitions: + CreateFunctionDetails: + type: object + properties: + traceConfig: + type: object + timeoutInSeconds: + type: integer + detachedModeTimeoutInSeconds: + type: integer + successDestination: + type: object + failureDestination: + type: object + UpdateFunctionDetails: + type: object + properties: + traceConfig: + type: object + timeoutInSeconds: + type: integer + detachedModeTimeoutInSeconds: + type: integer + successDestination: + type: object + failureDestination: + type: object +` + if err := os.WriteFile(specPath, []byte(spec), 0o644); err != nil { + t.Fatal(err) + } + files, err := GenerateFnFiles(specPath) + if err != nil { + t.Fatalf("GenerateFnFiles() error = %v", err) + } + flags := files[filepath.ToSlash("objects/fn/generated_oci_parity_flags.go")] + for _, expected := range []string{ + "trace-config", + "timeout-in-seconds", + "detached-mode-timeout-in-seconds", + "success-destination", + "failure-destination", + "display-name", + "sort-order", + } { + if !strings.Contains(flags, expected) { + t.Fatalf("generated function flags missing %q:\n%s", expected, flags) + } + } + list := files[filepath.ToSlash("objects/fn/generated_oci_parity_list.go")] + if !strings.Contains(list, "ApplyGeneratedOCIParityFnListParams") || !strings.Contains(list, "params.SortOrder") { + t.Fatalf("generated function list helper missing expected content:\n%s", list) + } + shim := files[filepath.ToSlash("vendor/github.com/fnproject/fn_go/provider/oracle/shim/generated_fn_oci_parity.go")] + for _, expected := range []string{ + "applyGeneratedOCIParityCreateFunctionDetails", + "applyGeneratedOCIParityListFunctionsRequest", + "parseGeneratedSuccessDestination", + "parseGeneratedFailureDestination", + } { + if !strings.Contains(shim, expected) { + t.Fatalf("generated function shim helper missing %q:\n%s", expected, shim) + } + } +} diff --git a/internal/ociparity/render.go b/internal/ociparity/render.go new file mode 100644 index 00000000..e904d717 --- /dev/null +++ b/internal/ociparity/render.go @@ -0,0 +1,373 @@ +package ociparity + +import ( + "fmt" + "strings" +) + +func renderHeader() string { + return "// Code generated by make generate-oci-parity; DO NOT EDIT.\n\n" +} + +func renderAppFlags(model *AppGenerationModel) string { + var b strings.Builder + b.WriteString(renderHeader()) + b.WriteString("package app\n\nimport \"github.com/urfave/cli\"\n\n") + b.WriteString("func GeneratedOCIParityCreateUpdateAppFlags() []cli.Flag {\n\treturn []cli.Flag{\n") + for _, f := range model.CreateUpdateFields { + switch f.Kind { + case "json": + fmt.Fprintf(&b, "\t\tcli.StringFlag{Name: %q, Usage: %q},\n", f.FlagName, fallbackDescription(f, "OCI parity advanced option")) + case "string-slice": + fmt.Fprintf(&b, "\t\tcli.StringSliceFlag{Name: %q, Usage: %q},\n", f.FlagName, fallbackDescription(f, "OCI parity advanced option")) + } + } + b.WriteString("\t}\n}\n\n") + b.WriteString("func GeneratedOCIParityListAppFlags() []cli.Flag {\n\treturn []cli.Flag{\n") + for _, f := range model.ListFields { + fmt.Fprintf(&b, "\t\tcli.StringFlag{Name: %q, Usage: %q},\n", f.FlagName, fallbackDescription(f, "OCI parity list option")) + } + b.WriteString("\t}\n}\n") + return b.String() +} + +func renderAppApply(model *AppGenerationModel) string { + var b strings.Builder + b.WriteString(renderHeader()) + b.WriteString("package app\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/fnproject/fn_go/modelsv2\"\n\t\"github.com/urfave/cli\"\n)\n\n") + b.WriteString("const (\n") + for _, f := range model.CreateUpdateFields { + fmt.Fprintf(&b, "\tannotationOCIParity%s = %q\n", exportName(f.Property), "oracle.com/oci/parity/"+f.Property) + } + b.WriteString(")\n\n") + b.WriteString(`func readGeneratedOCIParityJSONInput(value string) (map[string]interface{}, error) { + trimmed := strings.TrimSpace(value) + if trimmed == "" { + return nil, nil + } + if strings.HasPrefix(trimmed, "file://") { + data, err := os.ReadFile(strings.TrimPrefix(trimmed, "file://")) + if err != nil { + return nil, err + } + trimmed = string(data) + } + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(trimmed), &parsed); err != nil { + return nil, fmt.Errorf("invalid OCI parity JSON input: %w", err) + } + return parsed, nil +} + +func ApplyGeneratedOCIParityAppFlags(c *cli.Context, app *modelsv2.App) error { + if app.Annotations == nil { + app.Annotations = make(map[string]interface{}) + } +`) + for _, f := range model.CreateUpdateFields { + switch f.Kind { + case "json": + fmt.Fprintf(&b, "\tif c.IsSet(%q) {\n\t\tparsed, err := readGeneratedOCIParityJSONInput(c.String(%q))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tapp.Annotations[annotationOCIParity%s] = parsed\n\t}\n", f.FlagName, f.FlagName, exportName(f.Property)) + case "string-slice": + fmt.Fprintf(&b, "\tif len(c.StringSlice(%q)) > 0 {\n\t\tvalues := make([]interface{}, 0, len(c.StringSlice(%q)))\n\t\tfor _, v := range c.StringSlice(%q) {\n\t\t\tvalues = append(values, strings.TrimSpace(v))\n\t\t}\n\t\tapp.Annotations[annotationOCIParity%s] = values\n\t}\n", f.FlagName, f.FlagName, f.FlagName, exportName(f.Property)) + } + } + b.WriteString("\treturn nil\n}\n") + return b.String() +} + +func renderAppList(model *AppGenerationModel) string { + var b strings.Builder + b.WriteString(renderHeader()) + b.WriteString("package app\n\nimport (\n\t\"strings\"\n\n\tapiapps \"github.com/fnproject/fn_go/clientv2/apps\"\n\t\"github.com/urfave/cli\"\n)\n\n") + b.WriteString("func ApplyGeneratedOCIParityAppListParams(c *cli.Context, params *apiapps.ListAppsParams) {\n") + for _, f := range model.ListFields { + fmt.Fprintf(&b, "\tif c.IsSet(%q) {\n\t\tvalue := strings.TrimSpace(c.String(%q))\n\t\tif value != \"\" {\n", f.FlagName, f.FlagName) + switch f.Property { + case "displayName": + b.WriteString("\t\t\tparams.DisplayName = &value\n") + case "id": + b.WriteString("\t\t\tparams.ID = &value\n") + case "lifecycleState": + b.WriteString("\t\t\tparams.LifecycleState = &value\n") + case "sortBy": + b.WriteString("\t\t\tparams.SortBy = &value\n") + case "sortOrder": + b.WriteString("\t\t\tparams.SortOrder = &value\n") + } + b.WriteString("\t\t}\n\t}\n") + } + b.WriteString("}\n") + return b.String() +} + +func renderAppShim(model *AppGenerationModel) string { + var b strings.Builder + b.WriteString(renderHeader()) + b.WriteString("package shim\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/oracle/oci-go-sdk/v65/functions\"\n)\n\n") + b.WriteString("const (\n") + for _, f := range model.CreateUpdateFields { + fmt.Fprintf(&b, "\tannotationOCIParity%s = %q\n", exportName(f.Property), "oracle.com/oci/parity/"+f.Property) + } + b.WriteString(")\n\n") + b.WriteString("func applyGeneratedOCIParityCreateApplicationDetails(details *functions.CreateApplicationDetails, annotations map[string]interface{}) error {\n") + b.WriteString(renderAppShimBody(model)) + b.WriteString("\treturn nil\n}\n\n") + b.WriteString("func applyGeneratedOCIParityUpdateApplicationDetails(details *functions.UpdateApplicationDetails, annotations map[string]interface{}) error {\n") + b.WriteString(renderAppShimBody(model)) + b.WriteString("\treturn nil\n}\n\n") + b.WriteString(`func applyGeneratedOCIParityListApplicationsRequest(params interface{ GetDisplayName() *string; GetID() *string; GetLifecycleState() *string; GetSortBy() *string; GetSortOrder() *string }, req *functions.ListApplicationsRequest) { + if params.GetDisplayName() != nil { req.DisplayName = params.GetDisplayName() } + if params.GetID() != nil { req.Id = params.GetID() } + if params.GetLifecycleState() != nil { req.LifecycleState = functions.ApplicationLifecycleStateEnum(*params.GetLifecycleState()) } + if params.GetSortBy() != nil { req.SortBy = functions.ListApplicationsSortByEnum(*params.GetSortBy()) } + if params.GetSortOrder() != nil { req.SortOrder = functions.ListApplicationsSortOrderEnum(*params.GetSortOrder()) } +} +`) + return b.String() +} + +func renderAppShimBody(model *AppGenerationModel) string { + var b strings.Builder + for _, f := range model.CreateUpdateFields { + constName := "annotationOCIParity" + exportName(f.Property) + switch f.Property { + case "traceConfig": + fmt.Fprintf(&b, "\tif raw, ok := annotations[%s]; ok {\n\t\tdata, err := json.Marshal(raw)\n\t\tif err != nil { return err }\n\t\tvar parsed functions.ApplicationTraceConfig\n\t\tif err := json.Unmarshal(data, &parsed); err != nil { return fmt.Errorf(\"invalid traceConfig parity annotation: %%w\", err) }\n\t\tdetails.TraceConfig = &parsed\n\t}\n", constName) + case "imagePolicyConfig": + fmt.Fprintf(&b, "\tif raw, ok := annotations[%s]; ok {\n\t\tdata, err := json.Marshal(raw)\n\t\tif err != nil { return err }\n\t\tvar parsed functions.ImagePolicyConfig\n\t\tif err := json.Unmarshal(data, &parsed); err != nil { return fmt.Errorf(\"invalid imagePolicyConfig parity annotation: %%w\", err) }\n\t\tdetails.ImagePolicyConfig = &parsed\n\t}\n", constName) + case "securityAttributes": + fmt.Fprintf(&b, "\tif raw, ok := annotations[%s]; ok {\n\t\tdata, err := json.Marshal(raw)\n\t\tif err != nil { return err }\n\t\tvar parsed map[string]map[string]interface{}\n\t\tif err := json.Unmarshal(data, &parsed); err != nil { return fmt.Errorf(\"invalid securityAttributes parity annotation: %%w\", err) }\n\t\tdetails.SecurityAttributes = parsed\n\t}\n", constName) + case "networkSecurityGroupIds": + fmt.Fprintf(&b, "\tif raw, ok := annotations[%s]; ok {\n\t\tvar ids []string\n\t\tswitch typed := raw.(type) {\n\t\tcase []interface{}:\n\t\t\tfor _, v := range typed { if s, ok := v.(string); ok { ids = append(ids, s) } }\n\t\tcase []string:\n\t\t\tids = append(ids, typed...)\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"invalid networkSecurityGroupIds parity annotation\")\n\t\t}\n\t\tdetails.NetworkSecurityGroupIds = ids\n\t}\n", constName) + } + } + return b.String() +} + +func renderFnFlags(model *FnGenerationModel) string { + var b strings.Builder + b.WriteString(renderHeader()) + b.WriteString("package fn\n\nimport \"github.com/urfave/cli\"\n\n") + b.WriteString("func GeneratedOCIParityCreateUpdateFnFlags() []cli.Flag {\n\treturn []cli.Flag{\n") + for _, f := range model.CreateUpdateFields { + switch f.Kind { + case "json": + fmt.Fprintf(&b, "\t\tcli.StringFlag{Name: %q, Usage: %q},\n", f.FlagName, fallbackDescription(f, "OCI parity advanced option")) + case "int": + fmt.Fprintf(&b, "\t\tcli.IntFlag{Name: %q, Usage: %q},\n", f.FlagName, fallbackDescription(f, "OCI parity advanced option")) + } + } + b.WriteString("\t}\n}\n\n") + b.WriteString("func GeneratedOCIParityListFnFlags() []cli.Flag {\n\treturn []cli.Flag{\n") + for _, f := range model.ListFields { + fmt.Fprintf(&b, "\t\tcli.StringFlag{Name: %q, Usage: %q},\n", f.FlagName, fallbackDescription(f, "OCI parity list option")) + } + b.WriteString("\t}\n}\n") + return b.String() +} + +func renderFnApply(model *FnGenerationModel) string { + var b strings.Builder + b.WriteString(renderHeader()) + b.WriteString("package fn\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\tmodels \"github.com/fnproject/fn_go/modelsv2\"\n\t\"github.com/urfave/cli\"\n)\n\n") + b.WriteString("const (\n") + for _, f := range model.CreateUpdateFields { + if f.Property != "timeoutInSeconds" { + fmt.Fprintf(&b, "\tannotationOCIParityFn%s = %q\n", exportName(f.Property), "oracle.com/oci/parity/"+f.Property) + } + } + b.WriteString(")\n\n") + b.WriteString(`func readGeneratedOCIParityFnJSONInput(value string) (map[string]interface{}, error) { + trimmed := strings.TrimSpace(value) + if trimmed == "" { + return nil, nil + } + if strings.HasPrefix(trimmed, "file://") { + data, err := os.ReadFile(strings.TrimPrefix(trimmed, "file://")) + if err != nil { + return nil, err + } + trimmed = string(data) + } + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(trimmed), &parsed); err != nil { + return nil, fmt.Errorf("invalid OCI parity JSON input: %w", err) + } + return parsed, nil +} + +func ApplyGeneratedOCIParityFnFlags(c *cli.Context, fn *models.Fn) error { + if fn.Annotations == nil { + fn.Annotations = make(map[string]interface{}) + } +`) + for _, f := range model.CreateUpdateFields { + switch f.Property { + case "traceConfig": + fmt.Fprintf(&b, "\tif c.IsSet(%q) {\n\t\tparsed, err := readGeneratedOCIParityFnJSONInput(c.String(%q))\n\t\tif err != nil { return err }\n\t\tfn.Annotations[annotationOCIParityFn%s] = parsed\n\t}\n", f.FlagName, f.FlagName, exportName(f.Property)) + case "timeoutInSeconds": + fmt.Fprintf(&b, "\tif c.IsSet(%q) {\n\t\tv := int32(c.Int(%q))\n\t\tfn.Timeout = &v\n\t}\n", f.FlagName, f.FlagName) + case "detachedModeTimeoutInSeconds": + fmt.Fprintf(&b, "\tif c.IsSet(%q) {\n\t\tfn.Annotations[annotationOCIParityFn%s] = c.Int(%q)\n\t}\n", f.FlagName, exportName(f.Property), f.FlagName) + case "successDestination", "failureDestination": + fmt.Fprintf(&b, "\tif c.IsSet(%q) {\n\t\tparsed, err := readGeneratedOCIParityFnJSONInput(c.String(%q))\n\t\tif err != nil { return err }\n\t\tfn.Annotations[annotationOCIParityFn%s] = parsed\n\t}\n", f.FlagName, f.FlagName, exportName(f.Property)) + } + } + b.WriteString("\treturn nil\n}\n") + return b.String() +} + +func renderFnList(model *FnGenerationModel) string { + var b strings.Builder + b.WriteString(renderHeader()) + b.WriteString("package fn\n\nimport (\n\t\"strings\"\n\n\tapifns \"github.com/fnproject/fn_go/clientv2/fns\"\n\t\"github.com/urfave/cli\"\n)\n\n") + b.WriteString("func ApplyGeneratedOCIParityFnListParams(c *cli.Context, params *apifns.ListFnsParams) {\n") + for _, f := range model.ListFields { + fmt.Fprintf(&b, "\tif c.IsSet(%q) {\n\t\tvalue := strings.TrimSpace(c.String(%q))\n\t\tif value != \"\" {\n", f.FlagName, f.FlagName) + switch f.Property { + case "displayName": + b.WriteString("\t\t\tparams.DisplayName = &value\n") + case "id": + b.WriteString("\t\t\tparams.ID = &value\n") + case "lifecycleState": + b.WriteString("\t\t\tparams.LifecycleState = &value\n") + case "sortBy": + b.WriteString("\t\t\tparams.SortBy = &value\n") + case "sortOrder": + b.WriteString("\t\t\tparams.SortOrder = &value\n") + } + b.WriteString("\t\t}\n\t}\n") + } + b.WriteString("}\n") + return b.String() +} + +func renderFnShim(model *FnGenerationModel) string { + var b strings.Builder + b.WriteString(renderHeader()) + b.WriteString("package shim\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/oracle/oci-go-sdk/v65/functions\"\n)\n\n") + b.WriteString("const (\n") + for _, f := range model.CreateUpdateFields { + if f.Property != "timeoutInSeconds" { + fmt.Fprintf(&b, "\tannotationOCIParityFn%s = %q\n", exportName(f.Property), "oracle.com/oci/parity/"+f.Property) + } + } + b.WriteString(")\n\n") + b.WriteString(`func parseGeneratedSuccessDestination(raw interface{}) (functions.SuccessDestinationDetails, error) { + data, err := json.Marshal(raw) + if err != nil { return nil, err } + var kindHolder map[string]interface{} + if err := json.Unmarshal(data, &kindHolder); err != nil { return nil, err } + kind, _ := kindHolder["kind"].(string) + kind = strings.ToUpper(strings.TrimSpace(kind)) + if kind == "NOTIFICATIONS" { + kind = "NOTIFICATION" + kindHolder["kind"] = kind + data, err = json.Marshal(kindHolder) + if err != nil { return nil, err } + } + switch kind { + case "STREAM": + var parsed functions.StreamSuccessDestinationDetails + if err := json.Unmarshal(data, &parsed); err != nil { return nil, err } + return parsed, nil + case "QUEUE": + var parsed functions.QueueSuccessDestinationDetails + if err := json.Unmarshal(data, &parsed); err != nil { return nil, err } + return parsed, nil + case "NOTIFICATION": + var parsed functions.NotificationSuccessDestinationDetails + if err := json.Unmarshal(data, &parsed); err != nil { return nil, err } + return parsed, nil + case "NONE": + var parsed functions.NoneSuccessDestinationDetails + if err := json.Unmarshal(data, &parsed); err != nil { return nil, err } + return parsed, nil + default: + return nil, fmt.Errorf("invalid successDestination parity annotation kind %q", kind) + } +} + +func parseGeneratedFailureDestination(raw interface{}) (functions.FailureDestinationDetails, error) { + data, err := json.Marshal(raw) + if err != nil { return nil, err } + var kindHolder map[string]interface{} + if err := json.Unmarshal(data, &kindHolder); err != nil { return nil, err } + kind, _ := kindHolder["kind"].(string) + kind = strings.ToUpper(strings.TrimSpace(kind)) + if kind == "NOTIFICATIONS" { + kind = "NOTIFICATION" + kindHolder["kind"] = kind + data, err = json.Marshal(kindHolder) + if err != nil { return nil, err } + } + switch kind { + case "STREAM": + var parsed functions.StreamFailureDestinationDetails + if err := json.Unmarshal(data, &parsed); err != nil { return nil, err } + return parsed, nil + case "QUEUE": + var parsed functions.QueueFailureDestinationDetails + if err := json.Unmarshal(data, &parsed); err != nil { return nil, err } + return parsed, nil + case "NOTIFICATION": + var parsed functions.NotificationFailureDestinationDetails + if err := json.Unmarshal(data, &parsed); err != nil { return nil, err } + return parsed, nil + case "NONE": + var parsed functions.NoneFailureDestinationDetails + if err := json.Unmarshal(data, &parsed); err != nil { return nil, err } + return parsed, nil + default: + return nil, fmt.Errorf("invalid failureDestination parity annotation kind %q", kind) + } +} +`) + b.WriteString("func applyGeneratedOCIParityCreateFunctionDetails(details *functions.CreateFunctionDetails, annotations map[string]interface{}) error {\n") + b.WriteString(renderFnShimBody(model)) + b.WriteString("\treturn nil\n}\n\n") + b.WriteString("func applyGeneratedOCIParityUpdateFunctionDetails(details *functions.UpdateFunctionDetails, annotations map[string]interface{}) error {\n") + b.WriteString(renderFnShimBody(model)) + b.WriteString("\treturn nil\n}\n\n") + b.WriteString(`func applyGeneratedOCIParityListFunctionsRequest(params interface{ GetDisplayName() *string; GetID() *string; GetLifecycleState() *string; GetSortBy() *string; GetSortOrder() *string }, req *functions.ListFunctionsRequest) { + if params.GetDisplayName() != nil { req.DisplayName = params.GetDisplayName() } + if params.GetID() != nil { req.Id = params.GetID() } + if params.GetLifecycleState() != nil { req.LifecycleState = functions.FunctionLifecycleStateEnum(*params.GetLifecycleState()) } + if params.GetSortBy() != nil { req.SortBy = functions.ListFunctionsSortByEnum(*params.GetSortBy()) } + if params.GetSortOrder() != nil { req.SortOrder = functions.ListFunctionsSortOrderEnum(*params.GetSortOrder()) } +} +`) + return b.String() +} + +func renderFnShimBody(model *FnGenerationModel) string { + var b strings.Builder + for _, f := range model.CreateUpdateFields { + switch f.Property { + case "traceConfig": + fmt.Fprintf(&b, "\tif raw, ok := annotations[annotationOCIParityFn%s]; ok {\n\t\tdata, err := json.Marshal(raw)\n\t\tif err != nil { return err }\n\t\tvar parsed functions.FunctionTraceConfig\n\t\tif err := json.Unmarshal(data, &parsed); err != nil { return fmt.Errorf(\"invalid traceConfig parity annotation: %%w\", err) }\n\t\tdetails.TraceConfig = &parsed\n\t}\n", exportName(f.Property)) + case "detachedModeTimeoutInSeconds": + fmt.Fprintf(&b, "\tif raw, ok := annotations[annotationOCIParityFn%s]; ok {\n\t\tswitch typed := raw.(type) {\n\t\tcase int:\n\t\t\tv := typed\n\t\t\tdetails.DetachedModeTimeoutInSeconds = &v\n\t\tcase int32:\n\t\t\tv := int(typed)\n\t\t\tdetails.DetachedModeTimeoutInSeconds = &v\n\t\tcase int64:\n\t\t\tv := int(typed)\n\t\t\tdetails.DetachedModeTimeoutInSeconds = &v\n\t\tcase float64:\n\t\t\tv := int(typed)\n\t\t\tdetails.DetachedModeTimeoutInSeconds = &v\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"invalid detachedModeTimeoutInSeconds parity annotation\")\n\t\t}\n\t}\n", exportName(f.Property)) + case "successDestination": + fmt.Fprintf(&b, "\tif raw, ok := annotations[annotationOCIParityFn%s]; ok {\n\t\tparsed, err := parseGeneratedSuccessDestination(raw)\n\t\tif err != nil { return fmt.Errorf(\"invalid successDestination parity annotation: %%w\", err) }\n\t\tdetails.SuccessDestination = parsed\n\t}\n", exportName(f.Property)) + case "failureDestination": + fmt.Fprintf(&b, "\tif raw, ok := annotations[annotationOCIParityFn%s]; ok {\n\t\tparsed, err := parseGeneratedFailureDestination(raw)\n\t\tif err != nil { return fmt.Errorf(\"invalid failureDestination parity annotation: %%w\", err) }\n\t\tdetails.FailureDestination = parsed\n\t}\n", exportName(f.Property)) + } + } + return b.String() +} + +func exportName(v string) string { + if v == "" { + return "" + } + return strings.ToUpper(v[:1]) + v[1:] +} + +func fallbackDescription(f GeneratedField, fallback string) string { + if strings.TrimSpace(f.Description) != "" { + return f.Description + } + return fallback +} diff --git a/internal/ociparity/spec.go b/internal/ociparity/spec.go new file mode 100644 index 00000000..c7944aa7 --- /dev/null +++ b/internal/ociparity/spec.go @@ -0,0 +1,101 @@ +package ociparity + +import ( + "encoding/json" + "fmt" + "os" + "strings" + + "gopkg.in/yaml.v2" +) + +type Spec struct { + Root map[string]interface{} +} + +func LoadSpec(path string) (*Spec, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var raw interface{} + if err := yaml.Unmarshal(data, &raw); err != nil { + return nil, err + } + normalized := normalizeYAML(raw) + root, ok := normalized.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("spec root is not an object") + } + return &Spec{Root: root}, nil +} + +func normalizeYAML(v interface{}) interface{} { + switch t := v.(type) { + case map[interface{}]interface{}: + m := make(map[string]interface{}, len(t)) + for k, v2 := range t { + m[fmt.Sprint(k)] = normalizeYAML(v2) + } + return m + case []interface{}: + out := make([]interface{}, len(t)) + for i, item := range t { + out[i] = normalizeYAML(item) + } + return out + default: + return v + } +} + +func (s *Spec) ResolveRef(ref string) (map[string]interface{}, error) { + if !strings.HasPrefix(ref, "#/") { + return nil, fmt.Errorf("unsupported ref %q", ref) + } + parts := strings.Split(strings.TrimPrefix(ref, "#/"), "/") + var cur interface{} = s.Root + for _, part := range parts { + obj, ok := cur.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("invalid ref path %q", ref) + } + cur, ok = obj[part] + if !ok { + return nil, fmt.Errorf("ref not found %q", ref) + } + } + obj, ok := cur.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("ref %q does not point to an object", ref) + } + return obj, nil +} + +func (s *Spec) Schema(name string) (map[string]interface{}, error) { + if components, ok := s.Root["components"].(map[string]interface{}); ok { + if schemas, ok := components["schemas"].(map[string]interface{}); ok { + if entry, ok := schemas[name].(map[string]interface{}); ok { + return entry, nil + } + } + } + if definitions, ok := s.Root["definitions"].(map[string]interface{}); ok { + if entry, ok := definitions[name].(map[string]interface{}); ok { + return entry, nil + } + } + return nil, fmt.Errorf("schema %q not found in components.schemas or definitions", name) +} + +func ParseObjectOrRef(s *Spec, node map[string]interface{}) (map[string]interface{}, error) { + if ref, ok := node["$ref"].(string); ok { + return s.ResolveRef(ref) + } + return node, nil +} + +func marshalNormalized(v interface{}) string { + b, _ := json.Marshal(v) + return string(b) +} diff --git a/objects/app/apps.go b/objects/app/apps.go index c08da749..ec3bb767 100644 --- a/objects/app/apps.go +++ b/objects/app/apps.go @@ -17,6 +17,8 @@ package app import ( + ociCommon "github.com/oracle/oci-go-sdk/v65/common" + ocifunctions "github.com/oracle/oci-go-sdk/v65/functions" "encoding/json" "errors" "fmt" @@ -47,6 +49,98 @@ type appsCmd struct { client *fnclient.Fn } +type appFromJSON struct { + DisplayName string `json:"displayName"` + Config map[string]string `json:"config"` + SyslogURL *string `json:"syslogUrl"` + Shape string `json:"shape"` + SubnetIds []string `json:"subnetIds"` + FreeformTags map[string]string `json:"freeformTags"` + DefinedTags common.OCIDefinedTags `json:"definedTags"` + TraceConfig map[string]interface{} `json:"traceConfig"` + NetworkSecurityGroupIds []string `json:"networkSecurityGroupIds"` + ImagePolicyConfig map[string]interface{} `json:"imagePolicyConfig"` + SecurityAttributes map[string]map[string]interface{} `json:"securityAttributes"` + IfMatch string `json:"ifMatch"` + WaitForState string `json:"waitForState"` + MaxWaitSeconds int `json:"maxWaitSeconds"` + WaitIntervalSeconds int `json:"waitIntervalSeconds"` +} + +type appDeleteFromJSON struct { + IfMatch string `json:"ifMatch"` + WaitForState string `json:"waitForState"` + MaxWaitSeconds int `json:"maxWaitSeconds"` + WaitIntervalSeconds int `json:"waitIntervalSeconds"` +} + +type appChangeCompartmentFromJSON struct { + CompartmentID string `json:"compartmentId"` + IfMatch string `json:"ifMatch"` + WaitForState string `json:"waitForState"` + MaxWaitSeconds int `json:"maxWaitSeconds"` + WaitIntervalSeconds int `json:"waitIntervalSeconds"` +} + +func applyAppFromJSON(app *modelsv2.App, control *common.OCIRequestControl, input *appFromJSON) { + if app == nil || input == nil { + return + } + if strings.TrimSpace(app.Name) == "" && strings.TrimSpace(input.DisplayName) != "" { + app.Name = strings.TrimSpace(input.DisplayName) + } + if len(input.Config) > 0 { + app.Config = input.Config + } + if input.SyslogURL != nil { + app.SyslogURL = input.SyslogURL + } + if input.Shape != "" { + app.Shape = input.Shape + } + app.Annotations = common.ApplyOCIResourceTagsToAnnotations(app.Annotations, input.FreeformTags, input.DefinedTags) + if app.Annotations == nil { + app.Annotations = map[string]interface{}{} + } + if len(input.SubnetIds) > 0 { + values := make([]interface{}, 0, len(input.SubnetIds)) + for _, id := range input.SubnetIds { + values = append(values, strings.TrimSpace(id)) + } + app.Annotations[annotationSubnet] = values + } + if input.TraceConfig != nil { + app.Annotations[annotationOCIParityTraceConfig] = input.TraceConfig + } + if len(input.NetworkSecurityGroupIds) > 0 { + values := make([]interface{}, 0, len(input.NetworkSecurityGroupIds)) + for _, id := range input.NetworkSecurityGroupIds { + values = append(values, strings.TrimSpace(id)) + } + app.Annotations[annotationOCIParityNetworkSecurityGroupIds] = values + } + if input.ImagePolicyConfig != nil { + app.Annotations[annotationOCIParityImagePolicyConfig] = input.ImagePolicyConfig + } + if input.SecurityAttributes != nil { + app.Annotations[annotationOCIParitySecurityAttributes] = input.SecurityAttributes + } + if control != nil { + if control.IfMatch == "" { + control.IfMatch = strings.TrimSpace(input.IfMatch) + } + if control.WaitForState == "" { + control.WaitForState = strings.ToUpper(strings.TrimSpace(input.WaitForState)) + } + if control.MaxWaitSeconds == 0 { + control.MaxWaitSeconds = input.MaxWaitSeconds + } + if control.WaitIntervalSeconds == 0 { + control.WaitIntervalSeconds = input.WaitIntervalSeconds + } + } +} + func printApps(c *cli.Context, apps []*modelsv2.App) error { outputFormat := strings.ToLower(c.String("output")) if outputFormat == "json" { @@ -90,6 +184,35 @@ func (a *appsCmd) list(c *cli.Context) error { // getApps returns an array of all apps in the given context and client func getApps(c *cli.Context, client *fnclient.Fn) ([]*modelsv2.App, error) { params := &apiapps.ListAppsParams{Context: context.Background()} + var fromJSON struct { + DisplayName string `json:"displayName"` + ID string `json:"id"` + LifecycleState string `json:"lifecycleState"` + SortBy string `json:"sortBy"` + SortOrder string `json:"sortOrder"` + } + if err := common.LoadCLIJSONInput(c.String("from-json"), &fromJSON); err != nil { + return nil, err + } + if strings.TrimSpace(fromJSON.DisplayName) != "" { + params.DisplayName = &fromJSON.DisplayName + } + if strings.TrimSpace(fromJSON.ID) != "" { + params.ID = &fromJSON.ID + } + if strings.TrimSpace(fromJSON.LifecycleState) != "" { + lifecycle := strings.TrimSpace(fromJSON.LifecycleState) + params.LifecycleState = &lifecycle + } + if strings.TrimSpace(fromJSON.SortBy) != "" { + sortBy := strings.TrimSpace(fromJSON.SortBy) + params.SortBy = &sortBy + } + if strings.TrimSpace(fromJSON.SortOrder) != "" { + sortOrder := strings.TrimSpace(fromJSON.SortOrder) + params.SortOrder = &sortOrder + } + ApplyGeneratedOCIParityAppListParams(c, params) var resApps []*modelsv2.App for { resp, err := client.Apps.ListApps(params) @@ -158,6 +281,9 @@ func appWithFlags(c *cli.Context, app *modelsv2.App) error { return err } app.Annotations = annotations + if err := ApplyGeneratedOCIParityAppFlags(c, app); err != nil { + return err + } if err := setSubnetIDAnnotations(app, c.StringSlice("subnet-id")); err != nil { return err } @@ -228,9 +354,16 @@ func validateSubnetIDCreateRequired(p provider.Provider, app *modelsv2.App) erro } func (a *appsCmd) create(c *cli.Context) error { + control := common.ExtractOCIRequestControl(c) + common.WarnUnsupportedOCIRequestControl(a.provider, control) app := &modelsv2.App{ Name: c.Args().Get(0), } + var fromJSON appFromJSON + if err := common.LoadCLIJSONInput(c.String("from-json"), &fromJSON); err != nil { + return err + } + applyAppFromJSON(app, &control, &fromJSON) if err := appWithFlags(c, app); err != nil { return err @@ -252,15 +385,28 @@ func (a *appsCmd) create(c *cli.Context) error { if err := validateSubnetIDCreateRequired(a.provider, app); err != nil { return err } - _, err := CreateApp(a.client, app) + createdApp, err := CreateAppWithControl(a.client, app, control) + if err != nil { + return err + } + if err := common.WaitForAppState(a.provider, createdApp.ID, control.WaitForState, control.MaxWaitSeconds, control.WaitIntervalSeconds); err != nil { + return err + } return err } // CreateApp creates a new app using the given client func CreateApp(a *fnclient.Fn, app *modelsv2.App) (*modelsv2.App, error) { + return CreateAppWithControl(a, app, common.OCIRequestControl{}) +} + +func CreateAppWithControl(a *fnclient.Fn, app *modelsv2.App, control common.OCIRequestControl) (*modelsv2.App, error) { resp, err := a.Apps.CreateApp(&apiapps.CreateAppParams{ - Context: context.Background(), - Body: app, + Context: context.Background(), + Body: app, + WaitForState: control.WaitForState, + MaxWaitSeconds: int64(control.MaxWaitSeconds), + WaitIntervalSeconds: int64(control.WaitIntervalSeconds), }) if err != nil { @@ -278,11 +424,18 @@ func CreateApp(a *fnclient.Fn, app *modelsv2.App) (*modelsv2.App, error) { func (a *appsCmd) update(c *cli.Context) error { appName := c.Args().First() + control := common.ExtractOCIRequestControl(c) + common.WarnUnsupportedOCIRequestControl(a.provider, control) app, err := GetAppByName(a.client, appName) if err != nil { return err } + var fromJSON appFromJSON + if err := common.LoadCLIJSONInput(c.String("from-json"), &fromJSON); err != nil { + return err + } + applyAppFromJSON(app, &control, &fromJSON) if err := validateSubnetIDUpdateSupported(a.provider, c.StringSlice("subnet-id")); err != nil { return err @@ -292,7 +445,11 @@ func (a *appsCmd) update(c *cli.Context) error { return err } - if _, err = PutApp(a.client, app.ID, app); err != nil { + updatedApp, err := PutAppWithControl(a.client, app.ID, app, control) + if err != nil { + return err + } + if err := common.WaitForAppState(a.provider, updatedApp.ID, control.WaitForState, control.MaxWaitSeconds, control.WaitIntervalSeconds); err != nil { return err } @@ -433,6 +590,24 @@ func (a *appsCmd) inspect(c *cli.Context) error { func (a *appsCmd) delete(c *cli.Context) error { appName := c.Args().First() + control := common.ExtractOCIRequestControl(c) + common.WarnUnsupportedOCIRequestControl(a.provider, control) + var fromJSON appDeleteFromJSON + if err := common.LoadCLIJSONInput(c.String("from-json"), &fromJSON); err != nil { + return err + } + if control.IfMatch == "" { + control.IfMatch = strings.TrimSpace(fromJSON.IfMatch) + } + if control.WaitForState == "" { + control.WaitForState = strings.ToUpper(strings.TrimSpace(fromJSON.WaitForState)) + } + if control.MaxWaitSeconds == 0 { + control.MaxWaitSeconds = fromJSON.MaxWaitSeconds + } + if control.WaitIntervalSeconds == 0 { + control.WaitIntervalSeconds = fromJSON.WaitIntervalSeconds + } if appName == "" { //return errors.New("App name required to delete") } @@ -472,8 +647,12 @@ func (a *appsCmd) delete(c *cli.Context) error { } _, err = a.client.Apps.DeleteApp(&apiapps.DeleteAppParams{ - Context: context.Background(), - AppID: app.ID, + Context: context.Background(), + AppID: app.ID, + IfMatch: control.IfMatch, + WaitForState: control.WaitForState, + MaxWaitSeconds: int64(control.MaxWaitSeconds), + WaitIntervalSeconds: int64(control.WaitIntervalSeconds), }) if err != nil { @@ -483,17 +662,97 @@ func (a *appsCmd) delete(c *cli.Context) error { } return err } + if err := common.WaitForAppState(a.provider, app.ID, control.WaitForState, control.MaxWaitSeconds, control.WaitIntervalSeconds); err != nil { + return err + } fmt.Println("App", appName, "deleted") return nil } +func (a *appsCmd) changeCompartment(c *cli.Context) error { + control := common.ExtractOCIRequestControl(c) + common.WarnUnsupportedOCIRequestControl(a.provider, control) + var fromJSON appChangeCompartmentFromJSON + if err := common.LoadCLIJSONInput(c.String("from-json"), &fromJSON); err != nil { + return err + } + compartmentID := strings.TrimSpace(c.String("compartment-id")) + if compartmentID == "" { + compartmentID = strings.TrimSpace(fromJSON.CompartmentID) + } + if compartmentID == "" { + return fmt.Errorf("compartment id is required") + } + if control.IfMatch == "" { + control.IfMatch = strings.TrimSpace(fromJSON.IfMatch) + } + if control.WaitForState == "" { + control.WaitForState = strings.ToUpper(strings.TrimSpace(fromJSON.WaitForState)) + } + if control.MaxWaitSeconds == 0 { + control.MaxWaitSeconds = fromJSON.MaxWaitSeconds + } + if control.WaitIntervalSeconds == 0 { + control.WaitIntervalSeconds = fromJSON.WaitIntervalSeconds + } + if !common.IsOracleProvider(a.provider) { + return fmt.Errorf("change-compartment is only supported with an oracle provider") + } + appName := c.Args().First() + appObj, err := GetAppByName(a.client, appName) + if err != nil { + return err + } + mgmtClient, err := common.BuildOCIManagementClient(a.provider) + if err != nil { + return err + } + if mgmtClient == nil { + return fmt.Errorf("unable to build OCI Functions management client") + } + req := ocifunctions.ChangeApplicationCompartmentRequest{ + ApplicationId: &appObj.ID, + ChangeApplicationCompartmentDetails: ocifunctions.ChangeApplicationCompartmentDetails{ + CompartmentId: &compartmentID, + }, + IfMatch: stringPtr(control.IfMatch), + } + _, err = mgmtClient.ChangeApplicationCompartment(context.Background(), req) + if err != nil { + return err + } + if err := common.WaitForAppState(a.provider, appObj.ID, control.WaitForState, control.MaxWaitSeconds, control.WaitIntervalSeconds); err != nil { + if serr, ok := err.(ociCommon.ServiceError); ok && serr.GetHTTPStatusCode() == 404 { + return nil + } + return err + } + fmt.Printf("App %s moved to compartment %s\n", appName, compartmentID) + return nil +} + +func stringPtr(value string) *string { + if strings.TrimSpace(value) == "" { + return nil + } + return &value +} + // PutApp updates the app with the given ID using the content of the provided app func PutApp(a *fnclient.Fn, appID string, app *modelsv2.App) (*modelsv2.App, error) { + return PutAppWithControl(a, appID, app, common.OCIRequestControl{}) +} + +func PutAppWithControl(a *fnclient.Fn, appID string, app *modelsv2.App, control common.OCIRequestControl) (*modelsv2.App, error) { resp, err := a.Apps.UpdateApp(&apiapps.UpdateAppParams{ - Context: context.Background(), - AppID: appID, - Body: app, + Context: context.Background(), + AppID: appID, + Body: app, + IfMatch: control.IfMatch, + WaitForState: control.WaitForState, + MaxWaitSeconds: int64(control.MaxWaitSeconds), + WaitIntervalSeconds: int64(control.WaitIntervalSeconds), }) if err != nil { diff --git a/objects/app/commands.go b/objects/app/commands.go index a2c35bd6..6f7d494e 100644 --- a/objects/app/commands.go +++ b/objects/app/commands.go @@ -44,7 +44,7 @@ func Create() cli.Command { }, ArgsUsage: "", Action: a.create, - Flags: []cli.Flag{ + Flags: append([]cli.Flag{ cli.StringSliceFlag{ Name: "config", Usage: "Application configuration", @@ -73,7 +73,11 @@ func Create() cli.Command { Name: "subnet-id", Usage: "Subnet OCID for OCI Functions applications (can be specified multiple times; maps to oracle.com/oci/subnetIds)", }, - }, + cli.StringFlag{Name: "from-json", Usage: "Provide operation input as inline JSON or file://path"}, + cli.StringFlag{Name: "wait-for-state", Usage: "Wait until the application reaches the given lifecycle state"}, + cli.IntFlag{Name: "max-wait-seconds", Usage: "Maximum seconds to wait for the requested lifecycle state"}, + cli.IntFlag{Name: "wait-interval-seconds", Usage: "Polling interval in seconds while waiting for lifecycle state"}, + }, GeneratedOCIParityCreateUpdateAppFlags()...), } } @@ -96,7 +100,7 @@ func List() cli.Command { return nil }, Action: a.list, - Flags: []cli.Flag{ + Flags: append([]cli.Flag{ cli.StringFlag{ Name: "cursor", Usage: "Pagination cursor", @@ -110,7 +114,11 @@ func List() cli.Command { Name: "output", Usage: "Output format (json)", }, - }, + cli.StringFlag{ + Name: "from-json", + Usage: "Provide operation input as inline JSON or file://path", + }, + }, GeneratedOCIParityListAppFlags()...), } } @@ -149,6 +157,47 @@ func Delete() cli.Command { Name: "recursive, r", Usage: "Delete this app and all associated resources (can fail part way through execution after deleting some resources without the ability to undo)", }, + cli.StringFlag{Name: "from-json", Usage: "Provide operation input as inline JSON or file://path"}, + cli.StringFlag{Name: "if-match", Usage: "Apply optimistic concurrency control using the provided etag"}, + cli.StringFlag{Name: "wait-for-state", Usage: "Wait until the application reaches the given lifecycle state"}, + cli.IntFlag{Name: "max-wait-seconds", Usage: "Maximum seconds to wait for the requested lifecycle state"}, + cli.IntFlag{Name: "wait-interval-seconds", Usage: "Polling interval in seconds while waiting for lifecycle state"}, + }, + } +} + +// ChangeCompartment app command +func ChangeCompartment() cli.Command { + a := appsCmd{} + return cli.Command{ + Name: "app", + Usage: "Move an OCI Functions application to another compartment", + Category: "MANAGEMENT COMMAND", + Description: "This command moves an OCI Functions application to another compartment in the same tenancy.", + Aliases: []string{"apps", "a"}, + Before: func(c *cli.Context) error { + provider, err := client.CurrentProvider() + if err != nil { + return err + } + a.provider = provider + a.client = provider.APIClientv2() + return nil + }, + ArgsUsage: "", + Action: a.changeCompartment, + Flags: []cli.Flag{ + cli.StringFlag{Name: "compartment-id", Usage: "Target compartment OCID"}, + cli.StringFlag{Name: "from-json", Usage: "Provide operation input as inline JSON or file://path"}, + cli.StringFlag{Name: "if-match", Usage: "Apply optimistic concurrency control using the provided etag"}, + cli.StringFlag{Name: "wait-for-state", Usage: "Wait until the application reaches the given lifecycle state"}, + cli.IntFlag{Name: "max-wait-seconds", Usage: "Maximum seconds to wait for the requested lifecycle state"}, + cli.IntFlag{Name: "wait-interval-seconds", Usage: "Polling interval in seconds while waiting for lifecycle state"}, + }, + BashComplete: func(c *cli.Context) { + if len(c.Args()) == 0 { + BashCompleteApps(c) + } }, } } @@ -223,7 +272,7 @@ func Update() cli.Command { }, ArgsUsage: "", Action: a.update, - Flags: []cli.Flag{ + Flags: append([]cli.Flag{ cli.StringSliceFlag{ Name: "config,c", Usage: "Application configuration", @@ -268,7 +317,12 @@ func Update() cli.Command { Name: "subnet-id", Usage: "Subnet OCID for OCI Functions applications (accepted for non-Oracle providers; Oracle-backed update currently not supported)", }, - }, + cli.StringFlag{Name: "from-json", Usage: "Provide operation input as inline JSON or file://path"}, + cli.StringFlag{Name: "if-match", Usage: "Apply optimistic concurrency control using the provided etag"}, + cli.StringFlag{Name: "wait-for-state", Usage: "Wait until the application reaches the given lifecycle state"}, + cli.IntFlag{Name: "max-wait-seconds", Usage: "Maximum seconds to wait for the requested lifecycle state"}, + cli.IntFlag{Name: "wait-interval-seconds", Usage: "Polling interval in seconds while waiting for lifecycle state"}, + }, GeneratedOCIParityCreateUpdateAppFlags()...), BashComplete: func(c *cli.Context) { args := c.Args() if len(args) == 0 { diff --git a/objects/app/generated_oci_parity_apply.go b/objects/app/generated_oci_parity_apply.go new file mode 100644 index 00000000..62c86bdd --- /dev/null +++ b/objects/app/generated_oci_parity_apply.go @@ -0,0 +1,74 @@ +// Code generated by make generate-oci-parity; DO NOT EDIT. + +package app + +import ( + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/fnproject/fn_go/modelsv2" + "github.com/urfave/cli" +) + +const ( + annotationOCIParityImagePolicyConfig = "oracle.com/oci/parity/imagePolicyConfig" + annotationOCIParityNetworkSecurityGroupIds = "oracle.com/oci/parity/networkSecurityGroupIds" + annotationOCIParitySecurityAttributes = "oracle.com/oci/parity/securityAttributes" + annotationOCIParityTraceConfig = "oracle.com/oci/parity/traceConfig" +) + +func readGeneratedOCIParityJSONInput(value string) (map[string]interface{}, error) { + trimmed := strings.TrimSpace(value) + if trimmed == "" { + return nil, nil + } + if strings.HasPrefix(trimmed, "file://") { + data, err := os.ReadFile(strings.TrimPrefix(trimmed, "file://")) + if err != nil { + return nil, err + } + trimmed = string(data) + } + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(trimmed), &parsed); err != nil { + return nil, fmt.Errorf("invalid OCI parity JSON input: %w", err) + } + return parsed, nil +} + +func ApplyGeneratedOCIParityAppFlags(c *cli.Context, app *modelsv2.App) error { + if app.Annotations == nil { + app.Annotations = make(map[string]interface{}) + } + if c.IsSet("image-policy-config") { + parsed, err := readGeneratedOCIParityJSONInput(c.String("image-policy-config")) + if err != nil { + return err + } + app.Annotations[annotationOCIParityImagePolicyConfig] = parsed + } + if len(c.StringSlice("network-security-group-ids")) > 0 { + values := make([]interface{}, 0, len(c.StringSlice("network-security-group-ids"))) + for _, v := range c.StringSlice("network-security-group-ids") { + values = append(values, strings.TrimSpace(v)) + } + app.Annotations[annotationOCIParityNetworkSecurityGroupIds] = values + } + if c.IsSet("security-attributes") { + parsed, err := readGeneratedOCIParityJSONInput(c.String("security-attributes")) + if err != nil { + return err + } + app.Annotations[annotationOCIParitySecurityAttributes] = parsed + } + if c.IsSet("trace-config") { + parsed, err := readGeneratedOCIParityJSONInput(c.String("trace-config")) + if err != nil { + return err + } + app.Annotations[annotationOCIParityTraceConfig] = parsed + } + return nil +} diff --git a/objects/app/generated_oci_parity_flags.go b/objects/app/generated_oci_parity_flags.go new file mode 100644 index 00000000..4c3b0dab --- /dev/null +++ b/objects/app/generated_oci_parity_flags.go @@ -0,0 +1,24 @@ +// Code generated by make generate-oci-parity; DO NOT EDIT. + +package app + +import "github.com/urfave/cli" + +func GeneratedOCIParityCreateUpdateAppFlags() []cli.Flag { + return []cli.Flag{ + cli.StringFlag{Name: "image-policy-config", Usage: "OCI parity advanced option"}, + cli.StringSliceFlag{Name: "network-security-group-ids", Usage: "The [OCID](/iaas/Content/General/Concepts/identifiers.htm)s of the Network Security Groups to add the application to."}, + cli.StringFlag{Name: "security-attributes", Usage: "Security attributes for this resource. Each key is predefined and scoped to a namespace.\nFor more information, see [Resource Tags](/iaas/Content/General/Concepts/resourcetags.htm).\n\nExample: `{\"Oracle-ZPR\": {\"MaxEgressCount\": {\"value\": \"42\", \"mode\": \"enforce\"}}}`"}, + cli.StringFlag{Name: "trace-config", Usage: "OCI parity advanced option"}, + } +} + +func GeneratedOCIParityListAppFlags() []cli.Flag { + return []cli.Flag{ + cli.StringFlag{Name: "display-name", Usage: "Filter applications by exact display name"}, + cli.StringFlag{Name: "id", Usage: "Filter applications by OCID"}, + cli.StringFlag{Name: "lifecycle-state", Usage: "Filter applications by lifecycle state"}, + cli.StringFlag{Name: "sort-by", Usage: "Sort applications by a supported field"}, + cli.StringFlag{Name: "sort-order", Usage: "Sort order for list results"}, + } +} diff --git a/objects/app/generated_oci_parity_list.go b/objects/app/generated_oci_parity_list.go new file mode 100644 index 00000000..244d7ea1 --- /dev/null +++ b/objects/app/generated_oci_parity_list.go @@ -0,0 +1,43 @@ +// Code generated by make generate-oci-parity; DO NOT EDIT. + +package app + +import ( + "strings" + + apiapps "github.com/fnproject/fn_go/clientv2/apps" + "github.com/urfave/cli" +) + +func ApplyGeneratedOCIParityAppListParams(c *cli.Context, params *apiapps.ListAppsParams) { + if c.IsSet("display-name") { + value := strings.TrimSpace(c.String("display-name")) + if value != "" { + params.DisplayName = &value + } + } + if c.IsSet("id") { + value := strings.TrimSpace(c.String("id")) + if value != "" { + params.ID = &value + } + } + if c.IsSet("lifecycle-state") { + value := strings.TrimSpace(c.String("lifecycle-state")) + if value != "" { + params.LifecycleState = &value + } + } + if c.IsSet("sort-by") { + value := strings.TrimSpace(c.String("sort-by")) + if value != "" { + params.SortBy = &value + } + } + if c.IsSet("sort-order") { + value := strings.TrimSpace(c.String("sort-order")) + if value != "" { + params.SortOrder = &value + } + } +} diff --git a/objects/fn/commands.go b/objects/fn/commands.go index 2b583973..579ed091 100644 --- a/objects/fn/commands.go +++ b/objects/fn/commands.go @@ -25,6 +25,54 @@ import ( "github.com/urfave/cli" ) +func createFnCommandFlags() []cli.Flag { + flags := append([]cli.Flag{}, FnFlags...) + flags = append(flags, + cli.StringFlag{Name: "from-json", Usage: "Provide operation input as inline JSON or file://path"}, + cli.StringFlag{Name: "wait-for-state", Usage: "Wait until the function reaches the given lifecycle state"}, + cli.IntFlag{Name: "max-wait-seconds", Usage: "Maximum seconds to wait for the requested lifecycle state"}, + cli.IntFlag{Name: "wait-interval-seconds", Usage: "Polling interval in seconds while waiting for lifecycle state"}, + ) + flags = append(flags, GeneratedOCIParityCreateUpdateFnFlags()...) + return flags +} + +func listFnCommandFlags() []cli.Flag { + flags := []cli.Flag{ + cli.StringFlag{Name: "cursor", Usage: "pagination cursor"}, + cli.Int64Flag{Name: "n", Usage: "number of functions to return", Value: int64(100)}, + cli.StringFlag{Name: "output", Usage: "Output format (json)", Value: ""}, + cli.StringFlag{Name: "from-json", Usage: "Provide operation input as inline JSON or file://path"}, + } + flags = append(flags, GeneratedOCIParityListFnFlags()...) + return flags +} + +func deleteFnCommandFlags() []cli.Flag { + return []cli.Flag{ + cli.BoolFlag{Name: "force, f", Usage: "Forces this delete (you will not be asked if you wish to continue with the delete)"}, + cli.BoolFlag{Name: "recursive, r", Usage: "Delete this function and all associated resources (can fail part way through execution after deleting some resources without the ability to undo)"}, + cli.StringFlag{Name: "from-json", Usage: "Provide operation input as inline JSON or file://path"}, + cli.StringFlag{Name: "if-match", Usage: "Apply optimistic concurrency control using the provided etag"}, + cli.StringFlag{Name: "wait-for-state", Usage: "Wait until the function reaches the given lifecycle state"}, + cli.IntFlag{Name: "max-wait-seconds", Usage: "Maximum seconds to wait for the requested lifecycle state"}, + cli.IntFlag{Name: "wait-interval-seconds", Usage: "Polling interval in seconds while waiting for lifecycle state"}, + } +} + +func updateFnCommandFlags() []cli.Flag { + flags := append([]cli.Flag{}, updateFnFlags...) + flags = append(flags, + cli.StringFlag{Name: "from-json", Usage: "Provide operation input as inline JSON or file://path"}, + cli.StringFlag{Name: "if-match", Usage: "Apply optimistic concurrency control using the provided etag"}, + cli.StringFlag{Name: "wait-for-state", Usage: "Wait until the function reaches the given lifecycle state"}, + cli.IntFlag{Name: "max-wait-seconds", Usage: "Maximum seconds to wait for the requested lifecycle state"}, + cli.IntFlag{Name: "wait-interval-seconds", Usage: "Polling interval in seconds while waiting for lifecycle state"}, + ) + flags = append(flags, GeneratedOCIParityCreateUpdateFnFlags()...) + return flags +} + // Create function command func Create() cli.Command { f := fnsCmd{} @@ -46,7 +94,7 @@ func Create() cli.Command { }, ArgsUsage: " [image]", Action: f.create, - Flags: FnFlags, + Flags: createFnCommandFlags(), BashComplete: func(c *cli.Context) { if len(c.Args()) == 0 { app.BashCompleteApps(c) @@ -76,22 +124,7 @@ func List() cli.Command { }, ArgsUsage: "", Action: f.list, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "cursor", - Usage: "pagination cursor", - }, - cli.Int64Flag{ - Name: "n", - Usage: "number of functions to return", - Value: int64(100), - }, - cli.StringFlag{ - Name: "output", - Usage: "Output format (json)", - Value: "", - }, - }, + Flags: listFnCommandFlags(), BashComplete: func(c *cli.Context) { switch len(c.Args()) { case 0: @@ -122,6 +155,7 @@ func Delete() cli.Command { }, ArgsUsage: " ", Action: f.delete, + Flags: deleteFnCommandFlags(), BashComplete: func(c *cli.Context) { switch len(c.Args()) { case 0: @@ -130,16 +164,6 @@ func Delete() cli.Command { BashCompleteFns(c) } }, - Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "force, f", - Usage: "Forces this delete (you will not be asked if you wish to continue with the delete)", - }, - cli.BoolFlag{ - Name: "recursive, r", - Usage: "Delete this function and all associated resources (can fail part way through execution after deleting some resources without the ability to undo)", - }, - }, } } @@ -219,7 +243,7 @@ func Update() cli.Command { }, ArgsUsage: " ", Action: f.update, - Flags: updateFnFlags, + Flags: updateFnCommandFlags(), BashComplete: func(c *cli.Context) { switch len(c.Args()) { case 0: diff --git a/objects/fn/fns.go b/objects/fn/fns.go index d7112d74..7c55ec46 100644 --- a/objects/fn/fns.go +++ b/objects/fn/fns.go @@ -78,6 +78,86 @@ type provisionedConcurrencyView struct { Count *int `json:"count,omitempty"` } +type sourceDetailsView struct { + SourceType string `json:"sourceType,omitempty"` + PbfListingID string `json:"pbfListingId,omitempty"` +} + +type fnFromJSON struct { + DisplayName string `json:"displayName"` + Image string `json:"image"` + MemoryInMBs uint64 `json:"memoryInMBs"` + Config map[string]string `json:"config"` + TimeoutInSeconds *int32 `json:"timeoutInSeconds"` + TraceConfig map[string]interface{} `json:"traceConfig"` + FreeformTags map[string]string `json:"freeformTags"` + DefinedTags common.OCIDefinedTags `json:"definedTags"` + IfMatch string `json:"ifMatch"` + WaitForState string `json:"waitForState"` + MaxWaitSeconds int `json:"maxWaitSeconds"` + WaitIntervalSeconds int `json:"waitIntervalSeconds"` +} + +type fnDeleteFromJSON struct { + IfMatch string `json:"ifMatch"` + WaitForState string `json:"waitForState"` + MaxWaitSeconds int `json:"maxWaitSeconds"` + WaitIntervalSeconds int `json:"waitIntervalSeconds"` +} + +func applyFnFromJSON(fn *models.Fn, control *common.OCIRequestControl, input *fnFromJSON) { + if fn == nil || input == nil { + return + } + if strings.TrimSpace(fn.Name) == "" && strings.TrimSpace(input.DisplayName) != "" { + fn.Name = strings.TrimSpace(input.DisplayName) + } + if strings.TrimSpace(input.Image) != "" { + fn.Image = strings.TrimSpace(input.Image) + } + if input.MemoryInMBs > 0 { + fn.Memory = input.MemoryInMBs + } + if len(input.Config) > 0 { + fn.Config = input.Config + } + if input.TimeoutInSeconds != nil { + fn.Timeout = input.TimeoutInSeconds + } + fn.Annotations = common.ApplyOCIResourceTagsToAnnotations(fn.Annotations, input.FreeformTags, input.DefinedTags) + if fn.Annotations == nil { + fn.Annotations = map[string]interface{}{} + } + if input.TraceConfig != nil { + fn.Annotations[annotationOCIParityFnTraceConfig] = input.TraceConfig + } + if control != nil { + if control.IfMatch == "" { + control.IfMatch = strings.TrimSpace(input.IfMatch) + } + if control.WaitForState == "" { + control.WaitForState = strings.ToUpper(strings.TrimSpace(input.WaitForState)) + } + if control.MaxWaitSeconds == 0 { + control.MaxWaitSeconds = input.MaxWaitSeconds + } + if control.WaitIntervalSeconds == 0 { + control.WaitIntervalSeconds = input.WaitIntervalSeconds + } + } +} + +func formatSourceDisplay(fn *models.Fn) string { + view := getSourceDetailsView(fn) + if view == nil { + return "" + } + if view.PbfListingID != "" { + return fmt.Sprintf("pbf:%s", view.PbfListingID) + } + return strings.ToLower(view.SourceType) +} + // 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 { @@ -382,9 +462,46 @@ func buildInspectFnMap(fn *models.Fn) (map[string]interface{}, error) { } inspect["provisionedConcurrency"] = pcValue } + if source := getSourceDetailsView(fn); source != nil { + sourceData, err := json.Marshal(source) + if err != nil { + return nil, err + } + var sourceValue map[string]interface{} + if err := json.Unmarshal(sourceData, &sourceValue); err != nil { + return nil, err + } + inspect["sourceDetails"] = sourceValue + } + if fn != nil && fn.Annotations != nil { + if trace, ok := fn.Annotations[annotationOCIParityFnTraceConfig]; ok { + inspect["traceConfig"] = trace + } + } return inspect, nil } +func getSourceDetailsView(fn *models.Fn) *sourceDetailsView { + if fn == nil || fn.Annotations == nil { + return nil + } + sourceTypeRaw, ok := fn.Annotations[annotationSourceType] + if !ok { + return nil + } + sourceType, ok := sourceTypeRaw.(string) + if !ok || strings.TrimSpace(sourceType) == "" { + return nil + } + view := &sourceDetailsView{SourceType: sourceType} + if listingRaw, ok := fn.Annotations[annotationPbfListingID]; ok { + if listingID, ok := listingRaw.(string); ok { + view.PbfListingID = listingID + } + } + return view +} + // WithSlash appends "/" to function path func WithSlash(p string) string { p = path.Clean(p) @@ -413,12 +530,14 @@ func printFunctions(c *cli.Context, fns []*models.Fn) error { ID string `json:"id"` ProvisionedConcurrency *provisionedConcurrencyView `json:"provisionedConcurrency,omitempty"` DetachedMode *detachedModeView `json:"detachedMode,omitempty"` + SourceDetails *sourceDetailsView `json:"sourceDetails,omitempty"` }{ Name: fn.Name, Image: fn.Image, ID: fn.ID, ProvisionedConcurrency: getProvisionedConcurrencyView(fn), DetachedMode: getDetachedModeView(fn), + SourceDetails: getSourceDetailsView(fn), }) } b, err := json.MarshalIndent(newFns, "", " ") @@ -428,7 +547,7 @@ 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", "PC", "\t", "DETACHED_TIMEOUT", "\t", "DESTINATIONS", "\t", "ID", "\n") + fmt.Fprint(w, "NAME", "\t", "IMAGE", "\t", "SOURCE", "\t", "PC", "\t", "DETACHED_TIMEOUT", "\t", "DESTINATIONS", "\t", "ID", "\n") for _, f := range fns { view := getDetachedModeView(f) @@ -436,7 +555,7 @@ func printFunctions(c *cli.Context, fns []*models.Fn) error { if view != nil { timeout = view.Timeout } - fmt.Fprint(w, f.Name, "\t", f.Image, "\t", formatProvisionedConcurrencyDisplay(f), "\t", timeout, "\t", formatDetachedDestinations(f), "\t", f.ID, "\t", "\n") + fmt.Fprint(w, f.Name, "\t", f.Image, "\t", formatSourceDisplay(f), "\t", formatProvisionedConcurrencyDisplay(f), "\t", timeout, "\t", formatDetachedDestinations(f), "\t", f.ID, "\t", "\n") } if err := w.Flush(); err != nil { return err @@ -495,6 +614,22 @@ func formatProvisionedConcurrencyDisplay(fn *models.Fn) string { } } +func buildCreateFnSuccessMessage(fn *models.Fn) string { + if fn == nil { + return "Successfully created function" + } + if source := getSourceDetailsView(fn); source != nil && strings.EqualFold(source.SourceType, "PRE_BUILT_FUNCTIONS") { + if source.PbfListingID != "" { + return fmt.Sprintf("Successfully created function: %s from PBF %s", fn.Name, source.PbfListingID) + } + return fmt.Sprintf("Successfully created function: %s from PBF", fn.Name) + } + if strings.TrimSpace(fn.Image) != "" { + return fmt.Sprintf("Successfully created function: %s with %s", fn.Name, fn.Image) + } + return fmt.Sprintf("Successfully created function: %s", fn.Name) +} + func (f *fnsCmd) list(c *cli.Context) error { resFns, err := getFns(c, f.client) if err != nil { @@ -514,6 +649,35 @@ func getFns(c *cli.Context, client *fnclient.Fn) ([]*modelsv2.Fn, error) { Context: context.Background(), AppID: &a.ID, } + var fromJSON struct { + DisplayName string `json:"displayName"` + ID string `json:"id"` + LifecycleState string `json:"lifecycleState"` + SortBy string `json:"sortBy"` + SortOrder string `json:"sortOrder"` + } + if err := common.LoadCLIJSONInput(c.String("from-json"), &fromJSON); err != nil { + return nil, err + } + if strings.TrimSpace(fromJSON.DisplayName) != "" { + params.DisplayName = &fromJSON.DisplayName + } + if strings.TrimSpace(fromJSON.ID) != "" { + params.ID = &fromJSON.ID + } + if strings.TrimSpace(fromJSON.LifecycleState) != "" { + lifecycle := strings.TrimSpace(fromJSON.LifecycleState) + params.LifecycleState = &lifecycle + } + if strings.TrimSpace(fromJSON.SortBy) != "" { + sortBy := strings.TrimSpace(fromJSON.SortBy) + params.SortBy = &sortBy + } + if strings.TrimSpace(fromJSON.SortOrder) != "" { + sortOrder := strings.TrimSpace(fromJSON.SortOrder) + params.SortOrder = &sortOrder + } + ApplyGeneratedOCIParityFnListParams(c, params) var resFns []*models.Fn for { @@ -606,7 +770,8 @@ func WithFuncFileV20180708(ff *common.FuncFileV20180708, fn *models.Fn) error { return err } } - if ff.ImageNameV20180708() != "" { // args take precedence + isPBFSource := ff.Deploy != nil && ff.Deploy.OCI != nil && ff.Deploy.OCI.PBF != nil && strings.TrimSpace(ff.Deploy.OCI.PBF.ListingID) != "" + if !isPBFSource && ff.ImageNameV20180708() != "" { // args take precedence fn.Image = ff.ImageNameV20180708() } if ff.Timeout != nil { @@ -627,6 +792,12 @@ func WithFuncFileV20180708(ff *common.FuncFileV20180708, fn *models.Fn) error { } if ff.Deploy != nil && ff.Deploy.OCI != nil { fn.Annotations = common.ApplyOCIResourceTagsToAnnotations(fn.Annotations, ff.Deploy.OCI.FreeformTags, ff.Deploy.OCI.DefinedTags) + if ff.Deploy.OCI.PBF != nil { + fn.Image = "" + if err := setPBFSourceAnnotations(fn, ff.Deploy.OCI.PBF.ListingID); err != nil { + return err + } + } } // do something with triggers here @@ -692,6 +863,23 @@ func resolvePBFMemory(memory uint64, minRequired *int64) (uint64, error) { return memory, nil } +// ResolvePBFMemoryForListing auto-selects or validates memory for a PBF-backed function. +func ResolvePBFMemoryForListing(p provider.Provider, fn *models.Fn, listingID string) error { + if fn == nil || strings.TrimSpace(listingID) == "" { + return nil + } + minMemory, err := fetchCurrentPBFMemoryRequirement(p, listingID) + if err != nil { + return fmt.Errorf("unable to determine PBF memory requirements: %w", err) + } + resolvedMemory, err := resolvePBFMemory(fn.Memory, minMemory) + if err != nil { + return err + } + fn.Memory = resolvedMemory + return nil +} + func buildFunctionsManagementClient(oracleProvider *fnprovideroracle.OracleProvider) (*ocifunctions.FunctionsManagementClient, error) { client, err := ocifunctions.NewFunctionsManagementClientWithConfigurationProvider(oracleProvider.ConfigurationProvider) if err != nil { @@ -748,6 +936,8 @@ func ApplyProvisionedConcurrency(p provider.Provider, fnID string, cfg *common.O } func (f *fnsCmd) create(c *cli.Context) error { + control := common.ExtractOCIRequestControl(c) + common.WarnUnsupportedOCIRequestControl(f.provider, control) appName := c.Args().Get(0) fnName := c.Args().Get(1) pbfListingID := strings.TrimSpace(c.String("pbf")) @@ -775,8 +965,16 @@ func (f *fnsCmd) create(c *cli.Context) error { fn := &models.Fn{} fn.Name = fnName fn.Image = c.Args().Get(2) + var fromJSON fnFromJSON + if err := common.LoadCLIJSONInput(c.String("from-json"), &fromJSON); err != nil { + return err + } + applyFnFromJSON(fn, &control, &fromJSON) WithFlags(c, fn) + if err := ApplyGeneratedOCIParityFnFlags(c, fn); err != nil { + return err + } annotations, err := common.ApplyOCIResourceTagFlagsToAnnotations( fn.Annotations, c.StringSlice("tag"), @@ -807,15 +1005,9 @@ func (f *fnsCmd) create(c *cli.Context) error { if err := setPBFSourceAnnotations(fn, pbfListingID); err != nil { return err } - minMemory, err := fetchCurrentPBFMemoryRequirement(f.provider, pbfListingID) - if err != nil { - return fmt.Errorf("unable to determine PBF memory requirements: %w", err) - } - resolvedMemory, err := resolvePBFMemory(fn.Memory, minMemory) - if err != nil { + if err := ResolvePBFMemoryForListing(f.provider, fn, pbfListingID); err != nil { return err } - fn.Memory = resolvedMemory } if pcConfig != nil { if !common.IsOracleProvider(f.provider) { @@ -861,12 +1053,22 @@ func (f *fnsCmd) create(c *cli.Context) error { return err } - _, err = CreateFn(f.client, a.ID, fn) + createdFn, err := CreateFnWithControl(f.client, a.ID, fn, control) + if err != nil { + return err + } + if err := common.WaitForFunctionState(f.provider, createdFn.ID, control.WaitForState, control.MaxWaitSeconds, control.WaitIntervalSeconds); err != nil { + return err + } return err } // CreateFn request func CreateFn(r *fnclient.Fn, appID string, fn *models.Fn) (*models.Fn, error) { + return CreateFnWithControl(r, appID, fn, common.OCIRequestControl{}) +} + +func CreateFnWithControl(r *fnclient.Fn, appID string, fn *models.Fn, control common.OCIRequestControl) (*models.Fn, error) { fn.AppID = appID if fn.Image != "" { err := common.ValidateTagImageName(fn.Image) @@ -876,8 +1078,11 @@ func CreateFn(r *fnclient.Fn, appID string, fn *models.Fn) (*models.Fn, error) { } resp, err := r.Fns.CreateFn(&apifns.CreateFnParams{ - Context: context.Background(), - Body: fn, + Context: context.Background(), + Body: fn, + WaitForState: control.WaitForState, + MaxWaitSeconds: int64(control.MaxWaitSeconds), + WaitIntervalSeconds: int64(control.WaitIntervalSeconds), }) if err != nil { @@ -890,12 +1095,16 @@ func CreateFn(r *fnclient.Fn, appID string, fn *models.Fn) (*models.Fn, error) { return nil, err } - fmt.Println("Successfully created function:", resp.Payload.Name, "with", resp.Payload.Image) + fmt.Println(buildCreateFnSuccessMessage(resp.Payload)) return resp.Payload, nil } // PutFn updates the fn with the given ID using the content of the provided fn func PutFn(f *fnclient.Fn, fnID string, fn *models.Fn) error { + return PutFnWithControl(f, fnID, fn, common.OCIRequestControl{}) +} + +func PutFnWithControl(f *fnclient.Fn, fnID string, fn *models.Fn, control common.OCIRequestControl) error { if fn.Image != "" { err := common.ValidateTagImageName(fn.Image) if err != nil { @@ -904,9 +1113,13 @@ func PutFn(f *fnclient.Fn, fnID string, fn *models.Fn) error { } _, err := f.Fns.UpdateFn(&apifns.UpdateFnParams{ - Context: context.Background(), - FnID: fnID, - Body: fn, + Context: context.Background(), + FnID: fnID, + Body: fn, + IfMatch: control.IfMatch, + WaitForState: control.WaitForState, + MaxWaitSeconds: int64(control.MaxWaitSeconds), + WaitIntervalSeconds: int64(control.WaitIntervalSeconds), }) if err != nil { @@ -956,6 +1169,8 @@ func GetFnByName(client *fnclient.Fn, appID, fnName string) (*models.Fn, error) } func (f *fnsCmd) update(c *cli.Context) error { + control := common.ExtractOCIRequestControl(c) + common.WarnUnsupportedOCIRequestControl(f.provider, control) appName := c.Args().Get(0) fnName := c.Args().Get(1) if strings.TrimSpace(c.String("pbf")) != "" { @@ -994,8 +1209,16 @@ func (f *fnsCmd) update(c *cli.Context) error { if err != nil { return err } + var fromJSON fnFromJSON + if err := common.LoadCLIJSONInput(c.String("from-json"), &fromJSON); err != nil { + return err + } + applyFnFromJSON(fn, &control, &fromJSON) WithFlags(c, fn) + if err := ApplyGeneratedOCIParityFnFlags(c, fn); err != nil { + return err + } annotations, err := common.ApplyOCIResourceTagFlagsToAnnotations( fn.Annotations, c.StringSlice("tag"), @@ -1042,7 +1265,7 @@ func (f *fnsCmd) update(c *cli.Context) error { } } - err = PutFn(f.client, fn.ID, fn) + err = PutFnWithControl(f.client, fn.ID, fn, control) if err != nil { return err } @@ -1053,6 +1276,9 @@ func (f *fnsCmd) update(c *cli.Context) error { return err } } + if err := common.WaitForFunctionState(f.provider, fn.ID, control.WaitForState, control.MaxWaitSeconds, control.WaitIntervalSeconds); err != nil { + return err + } fmt.Println(appName, fnName, "updated") return nil @@ -1211,6 +1437,24 @@ func (f *fnsCmd) inspect(c *cli.Context) error { } func (f *fnsCmd) delete(c *cli.Context) error { + control := common.ExtractOCIRequestControl(c) + common.WarnUnsupportedOCIRequestControl(f.provider, control) + var fromJSON fnDeleteFromJSON + if err := common.LoadCLIJSONInput(c.String("from-json"), &fromJSON); err != nil { + return err + } + if control.IfMatch == "" { + control.IfMatch = strings.TrimSpace(fromJSON.IfMatch) + } + if control.WaitForState == "" { + control.WaitForState = strings.ToUpper(strings.TrimSpace(fromJSON.WaitForState)) + } + if control.MaxWaitSeconds == 0 { + control.MaxWaitSeconds = fromJSON.MaxWaitSeconds + } + if control.WaitIntervalSeconds == 0 { + control.WaitIntervalSeconds = fromJSON.WaitIntervalSeconds + } appName := c.Args().Get(0) fnName := c.Args().Get(1) @@ -1250,11 +1494,18 @@ func (f *fnsCmd) delete(c *cli.Context) error { params := apifns.NewDeleteFnParams() params.FnID = fn.ID + params.IfMatch = control.IfMatch + params.WaitForState = control.WaitForState + params.MaxWaitSeconds = int64(control.MaxWaitSeconds) + params.WaitIntervalSeconds = int64(control.WaitIntervalSeconds) _, err = f.client.Fns.DeleteFn(params) if err != nil { return err } + if err := common.WaitForFunctionState(f.provider, fn.ID, control.WaitForState, control.MaxWaitSeconds, control.WaitIntervalSeconds); err != nil { + return err + } fmt.Println("Function", fnName, "deleted") return nil diff --git a/objects/fn/fns_test.go b/objects/fn/fns_test.go index 9418de17..2bcd3e74 100644 --- a/objects/fn/fns_test.go +++ b/objects/fn/fns_test.go @@ -153,3 +153,52 @@ func TestResolvePBFMemory(t *testing.T) { t.Fatal("expected an error when memory is below the PBF minimum") } } + +func TestFormatSourceDisplay(t *testing.T) { + fn := &models.Fn{Annotations: map[string]interface{}{ + annotationSourceType: "PRE_BUILT_FUNCTIONS", + annotationPbfListingID: "ocid1.pbflisting.oc1..example", + }} + if got := formatSourceDisplay(fn); got != "pbf:ocid1.pbflisting.oc1..example" { + t.Fatalf("expected PBF source display, got %q", got) + } +} + +func TestBuildCreateFnSuccessMessage(t *testing.T) { + imgFn := &models.Fn{Name: "hello", Image: "repo/hello:0.0.1"} + if got := buildCreateFnSuccessMessage(imgFn); got != "Successfully created function: hello with repo/hello:0.0.1" { + t.Fatalf("unexpected image success message: %q", got) + } + pbfFn := &models.Fn{Name: "hello-pbf", Annotations: map[string]interface{}{ + annotationSourceType: "PRE_BUILT_FUNCTIONS", + annotationPbfListingID: "ocid1.pbflisting.oc1..example", + }} + if got := buildCreateFnSuccessMessage(pbfFn); got != "Successfully created function: hello-pbf from PBF ocid1.pbflisting.oc1..example" { + t.Fatalf("unexpected PBF success message: %q", got) + } +} + +func TestWithFuncFileV20180708AppliesPBFSource(t *testing.T) { + ff := &common.FuncFileV20180708{ + Name: "hello-pbf", + Version: "0.0.1", + Deploy: &common.FuncDeployConfig{ + OCI: &common.OCIFunctionDeployConfig{ + PBF: &common.OCIPBFSourceConfig{ListingID: "ocid1.pbflisting.oc1..example"}, + }, + }, + } + fn := &models.Fn{} + if err := WithFuncFileV20180708(ff, fn); err != nil { + t.Fatalf("WithFuncFileV20180708() error = %v", err) + } + if got := fn.Annotations[annotationSourceType]; got != "PRE_BUILT_FUNCTIONS" { + t.Fatalf("expected PRE_BUILT_FUNCTIONS source type, got %#v", got) + } + if got := fn.Annotations[annotationPbfListingID]; got != "ocid1.pbflisting.oc1..example" { + t.Fatalf("expected pbf listing id annotation, got %#v", got) + } + if fn.Image != "" { + t.Fatalf("expected PBF-backed function to avoid image assignment during deploy/init flows, got %q", fn.Image) + } +} diff --git a/objects/fn/generated_oci_parity_apply.go b/objects/fn/generated_oci_parity_apply.go new file mode 100644 index 00000000..8a046a11 --- /dev/null +++ b/objects/fn/generated_oci_parity_apply.go @@ -0,0 +1,68 @@ +// Code generated by make generate-oci-parity; DO NOT EDIT. + +package fn + +import ( + "encoding/json" + "fmt" + "os" + "strings" + + models "github.com/fnproject/fn_go/modelsv2" + "github.com/urfave/cli" +) + +const ( + annotationOCIParityFnDetachedModeTimeoutInSeconds = "oracle.com/oci/parity/detachedModeTimeoutInSeconds" + annotationOCIParityFnFailureDestination = "oracle.com/oci/parity/failureDestination" + annotationOCIParityFnSuccessDestination = "oracle.com/oci/parity/successDestination" + annotationOCIParityFnTraceConfig = "oracle.com/oci/parity/traceConfig" +) + +func readGeneratedOCIParityFnJSONInput(value string) (map[string]interface{}, error) { + trimmed := strings.TrimSpace(value) + if trimmed == "" { + return nil, nil + } + if strings.HasPrefix(trimmed, "file://") { + data, err := os.ReadFile(strings.TrimPrefix(trimmed, "file://")) + if err != nil { + return nil, err + } + trimmed = string(data) + } + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(trimmed), &parsed); err != nil { + return nil, fmt.Errorf("invalid OCI parity JSON input: %w", err) + } + return parsed, nil +} + +func ApplyGeneratedOCIParityFnFlags(c *cli.Context, fn *models.Fn) error { + if fn.Annotations == nil { + fn.Annotations = make(map[string]interface{}) + } + if c.IsSet("detached-mode-timeout-in-seconds") { + fn.Annotations[annotationOCIParityFnDetachedModeTimeoutInSeconds] = c.Int("detached-mode-timeout-in-seconds") + } + if c.IsSet("failure-destination") { + parsed, err := readGeneratedOCIParityFnJSONInput(c.String("failure-destination")) + if err != nil { return err } + fn.Annotations[annotationOCIParityFnFailureDestination] = parsed + } + if c.IsSet("success-destination") { + parsed, err := readGeneratedOCIParityFnJSONInput(c.String("success-destination")) + if err != nil { return err } + fn.Annotations[annotationOCIParityFnSuccessDestination] = parsed + } + if c.IsSet("timeout-in-seconds") { + v := int32(c.Int("timeout-in-seconds")) + fn.Timeout = &v + } + if c.IsSet("trace-config") { + parsed, err := readGeneratedOCIParityFnJSONInput(c.String("trace-config")) + if err != nil { return err } + fn.Annotations[annotationOCIParityFnTraceConfig] = parsed + } + return nil +} diff --git a/objects/fn/generated_oci_parity_flags.go b/objects/fn/generated_oci_parity_flags.go new file mode 100644 index 00000000..6f07dc61 --- /dev/null +++ b/objects/fn/generated_oci_parity_flags.go @@ -0,0 +1,25 @@ +// Code generated by make generate-oci-parity; DO NOT EDIT. + +package fn + +import "github.com/urfave/cli" + +func GeneratedOCIParityCreateUpdateFnFlags() []cli.Flag { + return []cli.Flag{ + cli.IntFlag{Name: "detached-mode-timeout-in-seconds", Usage: "Timeout for detached function invocations. Value in seconds."}, + cli.StringFlag{Name: "failure-destination", Usage: "OCI parity advanced option"}, + cli.StringFlag{Name: "success-destination", Usage: "OCI parity advanced option"}, + cli.IntFlag{Name: "timeout-in-seconds", Usage: "Timeout for executions of the function. Value in seconds."}, + cli.StringFlag{Name: "trace-config", Usage: "OCI parity advanced option"}, + } +} + +func GeneratedOCIParityListFnFlags() []cli.Flag { + return []cli.Flag{ + cli.StringFlag{Name: "display-name", Usage: "Filter functions by exact display name"}, + cli.StringFlag{Name: "id", Usage: "Filter functions by OCID"}, + cli.StringFlag{Name: "lifecycle-state", Usage: "Filter functions by lifecycle state"}, + cli.StringFlag{Name: "sort-by", Usage: "Sort functions by a supported field"}, + cli.StringFlag{Name: "sort-order", Usage: "Sort order for list results"}, + } +} diff --git a/objects/fn/generated_oci_parity_list.go b/objects/fn/generated_oci_parity_list.go new file mode 100644 index 00000000..75e25c9a --- /dev/null +++ b/objects/fn/generated_oci_parity_list.go @@ -0,0 +1,43 @@ +// Code generated by make generate-oci-parity; DO NOT EDIT. + +package fn + +import ( + "strings" + + apifns "github.com/fnproject/fn_go/clientv2/fns" + "github.com/urfave/cli" +) + +func ApplyGeneratedOCIParityFnListParams(c *cli.Context, params *apifns.ListFnsParams) { + if c.IsSet("display-name") { + value := strings.TrimSpace(c.String("display-name")) + if value != "" { + params.DisplayName = &value + } + } + if c.IsSet("id") { + value := strings.TrimSpace(c.String("id")) + if value != "" { + params.ID = &value + } + } + if c.IsSet("lifecycle-state") { + value := strings.TrimSpace(c.String("lifecycle-state")) + if value != "" { + params.LifecycleState = &value + } + } + if c.IsSet("sort-by") { + value := strings.TrimSpace(c.String("sort-by")) + if value != "" { + params.SortBy = &value + } + } + if c.IsSet("sort-order") { + value := strings.TrimSpace(c.String("sort-order")) + if value != "" { + params.SortOrder = &value + } + } +} diff --git a/objects/fn/output_test.go b/objects/fn/output_test.go index cc9c1623..30c0a679 100644 --- a/objects/fn/output_test.go +++ b/objects/fn/output_test.go @@ -11,10 +11,10 @@ import ( func TestGetDetachedModeView(t *testing.T) { fn := &models.Fn{Annotations: map[string]interface{}{ annotationDetachedTimeoutSeconds: 1200, - annotationSuccessDestinationKind: "STREAM", - annotationSuccessDestinationOCID: "ocid1.stream.oc1..abc", - annotationFailureDestinationKind: "NOTIFICATIONS", - annotationFailureDestinationOCID: "ocid1.onstopic.oc1..abc", + annotationSuccessDestinationKind: "STREAM", + annotationSuccessDestinationOCID: "ocid1.stream.oc1..abc", + annotationFailureDestinationKind: "NOTIFICATIONS", + annotationFailureDestinationOCID: "ocid1.onstopic.oc1..abc", }} view := getDetachedModeView(fn) if view == nil { @@ -34,8 +34,8 @@ func TestGetDetachedModeView(t *testing.T) { func TestBuildInspectFnMapIncludesDetachedMode(t *testing.T) { fn := &models.Fn{Annotations: map[string]interface{}{ annotationDetachedTimeoutSeconds: 1200, - annotationSuccessDestinationKind: "STREAM", - annotationSuccessDestinationOCID: "ocid1.stream.oc1..abc", + annotationSuccessDestinationKind: "STREAM", + annotationSuccessDestinationOCID: "ocid1.stream.oc1..abc", }} inspect, err := buildInspectFnMap(fn) if err != nil { @@ -61,8 +61,8 @@ func TestBuildInspectFnMapIncludesDetachedMode(t *testing.T) { func TestBuildInspectFnMapSupportsNestedDetachedModeQuery(t *testing.T) { fn := &models.Fn{Annotations: map[string]interface{}{ annotationDetachedTimeoutSeconds: 1200, - annotationSuccessDestinationKind: "STREAM", - annotationSuccessDestinationOCID: "ocid1.stream.oc1..abc", + annotationSuccessDestinationKind: "STREAM", + annotationSuccessDestinationOCID: "ocid1.stream.oc1..abc", }} inspect, err := buildInspectFnMap(fn) if err != nil { @@ -76,4 +76,31 @@ func TestBuildInspectFnMapSupportsNestedDetachedModeQuery(t *testing.T) { if value != "20m" { t.Fatalf("expected timeout 20m, got %#v", value) } -} \ No newline at end of file +} + +func TestBuildInspectFnMapIncludesTraceConfig(t *testing.T) { + fn := &models.Fn{Annotations: map[string]interface{}{ + annotationOCIParityFnTraceConfig: map[string]interface{}{ + "isEnabled": true, + }, + }} + inspect, err := buildInspectFnMap(fn) + if err != nil { + t.Fatalf("buildInspectFnMap() error = %v", err) + } + trace, ok := inspect["traceConfig"] + if !ok { + t.Fatal("expected traceConfig field in inspect map") + } + data, err := json.Marshal(trace) + if err != nil { + t.Fatalf("failed to marshal traceConfig: %v", err) + } + var got map[string]interface{} + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("failed to unmarshal traceConfig: %v", err) + } + if got["isEnabled"] != true { + t.Fatalf("expected isEnabled=true in traceConfig, got %#v", got["isEnabled"]) + } +} diff --git a/objects/pbf/commands.go b/objects/pbf/commands.go new file mode 100644 index 00000000..0da90b71 --- /dev/null +++ b/objects/pbf/commands.go @@ -0,0 +1,107 @@ +package pbf + +import "github.com/urfave/cli" + +func sharedListFlags() []cli.Flag { + return []cli.Flag{ + cli.IntFlag{Name: "limit", Usage: "Maximum number of items to return", Value: 10}, + cli.StringFlag{Name: "output", Usage: "Output format (json)"}, + } +} + +func List() cli.Command { + return cli.Command{ + Name: "pbfs", + Usage: "List Pre-Built Function listings and related resources", + Description: "List available OCI Pre-Built Functions (PBFs), their versions, or supported triggers.", + Before: func(c *cli.Context) error { + _, err := initPBFClient() + return err + }, + Action: func(c *cli.Context) error { + cmd, err := initPBFClient() + if err != nil { + return err + } + return cmd.listListings(c) + }, + Flags: append(sharedListFlags(), + cli.StringFlag{Name: "search", Usage: "Filter listings by partial name match"}, + cli.StringFlag{Name: "trigger", Usage: "Filter listings by trigger name"}, + ), + Subcommands: []cli.Command{ + { + Name: "versions", + Usage: "List versions for a PBF listing", + ArgsUsage: "", + Before: func(c *cli.Context) error { + _, err := initPBFClient() + return err + }, + Action: func(c *cli.Context) error { + cmd, err := initPBFClient() + if err != nil { + return err + } + return cmd.listVersions(c) + }, + Flags: append(sharedListFlags(), cli.BoolFlag{Name: "latest", Usage: "Show only the current/latest active version"}), + }, + { + Name: "triggers", + Usage: "List supported PBF trigger names", + ArgsUsage: "[trigger-name]", + Before: func(c *cli.Context) error { + _, err := initPBFClient() + return err + }, + Action: func(c *cli.Context) error { + cmd, err := initPBFClient() + if err != nil { + return err + } + return cmd.listTriggers(c) + }, + Flags: sharedListFlags(), + }, + }, + } +} + +func Get() cli.Command { + return cli.Command{ + Name: "pbfs", + Usage: "Get a Pre-Built Function listing or version", + Description: "Get detailed information for an OCI Pre-Built Function (PBF) listing or a specific listing version.", + ArgsUsage: "", + Before: func(c *cli.Context) error { + _, err := initPBFClient() + return err + }, + Action: func(c *cli.Context) error { + cmd, err := initPBFClient() + if err != nil { + return err + } + return cmd.getListing(c) + }, + Subcommands: []cli.Command{ + { + Name: "version", + Usage: "Get a specific PBF listing version by OCID", + ArgsUsage: "", + Before: func(c *cli.Context) error { + _, err := initPBFClient() + return err + }, + Action: func(c *cli.Context) error { + cmd, err := initPBFClient() + if err != nil { + return err + } + return cmd.getVersion(c) + }, + }, + }, + } +} diff --git a/objects/pbf/pbfs.go b/objects/pbf/pbfs.go new file mode 100644 index 00000000..40161e7f --- /dev/null +++ b/objects/pbf/pbfs.go @@ -0,0 +1,276 @@ +package pbf + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + "text/tabwriter" + "time" + + cliClient "github.com/fnproject/cli/client" + fnprovideroracle "github.com/fnproject/fn_go/provider/oracle" + ocifunctions "github.com/oracle/oci-go-sdk/v65/functions" + "github.com/urfave/cli" +) + +type pbfCmd struct { + provider *fnprovideroracle.OracleProvider + client *ocifunctions.FunctionsManagementClient +} + +func buildFunctionsManagementClient(provider *fnprovideroracle.OracleProvider) (*ocifunctions.FunctionsManagementClient, error) { + client, err := ocifunctions.NewFunctionsManagementClientWithConfigurationProvider(provider.ConfigurationProvider) + if err != nil { + return nil, err + } + if provider.FnApiUrl != nil { + client.Host = provider.FnApiUrl.String() + } else { + region, _ := provider.ConfigurationProvider.Region() + if region != "" { + client.SetRegion(region) + } + } + return &client, nil +} + +func initPBFClient() (*pbfCmd, error) { + provider, err := cliClient.CurrentProvider() + if err != nil { + return nil, err + } + oracleProvider, ok := provider.(*fnprovideroracle.OracleProvider) + if !ok || oracleProvider == nil { + return nil, fmt.Errorf("PBF commands are only supported with an oracle provider") + } + mgmtClient, err := buildFunctionsManagementClient(oracleProvider) + if err != nil { + return nil, err + } + return &pbfCmd{provider: oracleProvider, client: mgmtClient}, nil +} + +func formatSDKTime(t *time.Time) string { + if t == nil || t.IsZero() { + return "" + } + return t.UTC().Format(time.RFC3339) +} + +func formatListingTriggers(triggers []ocifunctions.Trigger) string { + if len(triggers) == 0 { + return "" + } + names := make([]string, 0, len(triggers)) + for _, trig := range triggers { + if trig.Name != nil { + names = append(names, *trig.Name) + } + } + return strings.Join(names, ",") +} + +func isOCID(value string) bool { + return strings.HasPrefix(strings.TrimSpace(value), "ocid1.") +} + +func (p *pbfCmd) resolveListingID(identifier string) (string, error) { + identifier = strings.TrimSpace(identifier) + if identifier == "" { + return "", fmt.Errorf("missing PBF listing identifier") + } + if isOCID(identifier) { + return identifier, nil + } + limit := 1 + req := ocifunctions.ListPbfListingsRequest{Name: &identifier, Limit: &limit} + res, err := p.client.ListPbfListings(context.Background(), req) + if err != nil { + return "", err + } + if len(res.Items) == 0 || res.Items[0].Id == nil { + return "", fmt.Errorf("PBF listing %q not found", identifier) + } + return *res.Items[0].Id, nil +} + +func printListings(c *cli.Context, items []ocifunctions.PbfListingSummary) error { + if strings.EqualFold(c.String("output"), "json") { + b, err := json.MarshalIndent(items, "", " ") + if err != nil { + return err + } + _, err = fmt.Fprint(os.Stdout, string(b)) + return err + } + w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0) + _, _ = fmt.Fprintln(w, "NAME\tPUBLISHER\tUPDATED\tTRIGGERS\tID") + for _, item := range items { + name := "" + if item.Name != nil { + name = *item.Name + } + publisher := "" + if item.PublisherDetails != nil && item.PublisherDetails.Name != nil { + publisher = *item.PublisherDetails.Name + } + updated := "" + if item.TimeUpdated != nil { + t := item.TimeUpdated.Time + updated = formatSDKTime(&t) + } + id := "" + if item.Id != nil { + id = *item.Id + } + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", name, publisher, updated, formatListingTriggers(item.Triggers), id) + } + return w.Flush() +} + +func printListingVersions(c *cli.Context, items []ocifunctions.PbfListingVersionSummary) error { + if strings.EqualFold(c.String("output"), "json") { + b, err := json.MarshalIndent(items, "", " ") + if err != nil { + return err + } + _, err = fmt.Fprint(os.Stdout, string(b)) + return err + } + w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0) + _, _ = fmt.Fprintln(w, "VERSION\tSTATE\tUPDATED\tMIN_MEMORY_MBS\tID") + for _, item := range items { + version := "" + if item.Name != nil { + version = *item.Name + } + updated := "" + if item.TimeUpdated != nil { + t := item.TimeUpdated.Time + updated = formatSDKTime(&t) + } + minMemory := "" + if item.Requirements != nil && item.Requirements.MinMemoryRequiredInMBs != nil { + minMemory = fmt.Sprintf("%d", *item.Requirements.MinMemoryRequiredInMBs) + } + id := "" + if item.Id != nil { + id = *item.Id + } + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", version, item.LifecycleState, updated, minMemory, id) + } + return w.Flush() +} + +func printPBFTriggers(c *cli.Context, items []ocifunctions.TriggerSummary) error { + if strings.EqualFold(c.String("output"), "json") { + b, err := json.MarshalIndent(items, "", " ") + if err != nil { + return err + } + _, err = fmt.Fprint(os.Stdout, string(b)) + return err + } + w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0) + _, _ = fmt.Fprintln(w, "NAME") + for _, item := range items { + name := "" + if item.Name != nil { + name = *item.Name + } + _, _ = fmt.Fprintf(w, "%s\n", name) + } + return w.Flush() +} + +func (p *pbfCmd) listListings(c *cli.Context) error { + limit := c.Int("limit") + req := ocifunctions.ListPbfListingsRequest{} + if limit > 0 { + req.Limit = &limit + } + if search := strings.TrimSpace(c.String("search")); search != "" { + req.NameContains = &search + } + if trigger := strings.TrimSpace(c.String("trigger")); trigger != "" { + req.Trigger = []string{trigger} + } + res, err := p.client.ListPbfListings(context.Background(), req) + if err != nil { + return err + } + return printListings(c, res.Items) +} + +func (p *pbfCmd) getListing(c *cli.Context) error { + listingID, err := p.resolveListingID(c.Args().First()) + if err != nil { + return err + } + res, err := p.client.GetPbfListing(context.Background(), ocifunctions.GetPbfListingRequest{PbfListingId: &listingID}) + if err != nil { + return err + } + b, err := json.MarshalIndent(res.PbfListing, "", " ") + if err != nil { + return err + } + _, err = fmt.Fprint(os.Stdout, string(b)) + return err +} + +func (p *pbfCmd) listVersions(c *cli.Context) error { + listingID, err := p.resolveListingID(c.Args().First()) + if err != nil { + return err + } + limit := c.Int("limit") + req := ocifunctions.ListPbfListingVersionsRequest{PbfListingId: &listingID} + if limit > 0 { + req.Limit = &limit + } + if c.Bool("latest") { + current := true + req.IsCurrentVersion = ¤t + } + res, err := p.client.ListPbfListingVersions(context.Background(), req) + if err != nil { + return err + } + return printListingVersions(c, res.Items) +} + +func (p *pbfCmd) getVersion(c *cli.Context) error { + versionID := strings.TrimSpace(c.Args().First()) + if versionID == "" { + return fmt.Errorf("missing PBF listing version identifier") + } + res, err := p.client.GetPbfListingVersion(context.Background(), ocifunctions.GetPbfListingVersionRequest{PbfListingVersionId: &versionID}) + if err != nil { + return err + } + b, err := json.MarshalIndent(res.PbfListingVersion, "", " ") + if err != nil { + return err + } + _, err = fmt.Fprint(os.Stdout, string(b)) + return err +} + +func (p *pbfCmd) listTriggers(c *cli.Context) error { + limit := c.Int("limit") + req := ocifunctions.ListTriggersRequest{} + if limit > 0 { + req.Limit = &limit + } + if name := strings.TrimSpace(c.Args().First()); name != "" { + req.Name = &name + } + res, err := p.client.ListTriggers(context.Background(), req) + if err != nil { + return err + } + return printPBFTriggers(c, res.Items) +} diff --git a/objects/pbf/pbfs_test.go b/objects/pbf/pbfs_test.go new file mode 100644 index 00000000..deb70ec7 --- /dev/null +++ b/objects/pbf/pbfs_test.go @@ -0,0 +1,34 @@ +package pbf + +import ( + "testing" + "time" + + ocifunctions "github.com/oracle/oci-go-sdk/v65/functions" +) + +func TestFormatListingTriggers(t *testing.T) { + name1 := "http" + name2 := "objectstorage" + got := formatListingTriggers([]ocifunctions.Trigger{{Name: &name1}, {Name: &name2}}) + if got != "http,objectstorage" { + t.Fatalf("expected trigger string, got %q", got) + } +} + +func TestFormatSDKTime(t *testing.T) { + ts := time.Date(2026, 5, 15, 12, 0, 0, 0, time.UTC) + got := formatSDKTime(&ts) + if got == "" { + t.Fatal("expected non-empty formatted time") + } +} + +func TestIsOCID(t *testing.T) { + if !isOCID("ocid1.pbflisting.oc1..example") { + t.Fatal("expected ocid to be recognized") + } + if isOCID("hello-world") { + t.Fatal("did not expect non-ocid to be recognized") + } +} diff --git a/tools/oci_parity_gen/main.go b/tools/oci_parity_gen/main.go new file mode 100644 index 00000000..d9510d6e --- /dev/null +++ b/tools/oci_parity_gen/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "flag" + "fmt" + "os" + "path/filepath" + + "github.com/fnproject/cli/internal/ociparity" +) + +func main() { + var specPath string + flag.StringVar(&specPath, "spec", os.Getenv("SPEC"), "Path to OCI Functions API spec") + flag.Parse() + if specPath == "" { + fmt.Fprintln(os.Stderr, "missing spec path; use SPEC=/absolute/path/to/functions-api-spec.yaml make generate-oci-parity") + os.Exit(1) + } + wd, err := os.Getwd() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + root := filepath.Clean(filepath.Join(wd)) + if err := ociparity.WriteGeneratedFiles(root, specPath); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/vendor/github.com/fnproject/fn_go/clientv2/apps/create_app_parameters.go b/vendor/github.com/fnproject/fn_go/clientv2/apps/create_app_parameters.go index ffc996cf..a96daa63 100644 --- a/vendor/github.com/fnproject/fn_go/clientv2/apps/create_app_parameters.go +++ b/vendor/github.com/fnproject/fn_go/clientv2/apps/create_app_parameters.go @@ -57,7 +57,8 @@ func NewCreateAppParamsWithHTTPClient(client *http.Client) *CreateAppParams { } } -/*CreateAppParams contains all the parameters to send to the API endpoint +/* +CreateAppParams contains all the parameters to send to the API endpoint for the create app operation typically these are written to a http.Request */ type CreateAppParams struct { @@ -66,7 +67,10 @@ type CreateAppParams struct { Application data to insert. */ - Body *modelsv2.App + Body *modelsv2.App + WaitForState string + MaxWaitSeconds int64 + WaitIntervalSeconds int64 timeout time.Duration Context context.Context diff --git a/vendor/github.com/fnproject/fn_go/clientv2/apps/delete_app_parameters.go b/vendor/github.com/fnproject/fn_go/clientv2/apps/delete_app_parameters.go index 62564e91..3a5a784d 100644 --- a/vendor/github.com/fnproject/fn_go/clientv2/apps/delete_app_parameters.go +++ b/vendor/github.com/fnproject/fn_go/clientv2/apps/delete_app_parameters.go @@ -55,7 +55,8 @@ func NewDeleteAppParamsWithHTTPClient(client *http.Client) *DeleteAppParams { } } -/*DeleteAppParams contains all the parameters to send to the API endpoint +/* +DeleteAppParams contains all the parameters to send to the API endpoint for the delete app operation typically these are written to a http.Request */ type DeleteAppParams struct { @@ -64,7 +65,11 @@ type DeleteAppParams struct { Opaque, unique Application ID. */ - AppID string + AppID string + IfMatch string + WaitForState string + MaxWaitSeconds int64 + WaitIntervalSeconds int64 timeout time.Duration Context context.Context diff --git a/vendor/github.com/fnproject/fn_go/clientv2/apps/list_apps_parameters.go b/vendor/github.com/fnproject/fn_go/clientv2/apps/list_apps_parameters.go index 6c5569da..2bfa5e0a 100644 --- a/vendor/github.com/fnproject/fn_go/clientv2/apps/list_apps_parameters.go +++ b/vendor/github.com/fnproject/fn_go/clientv2/apps/list_apps_parameters.go @@ -56,7 +56,8 @@ func NewListAppsParamsWithHTTPClient(client *http.Client) *ListAppsParams { } } -/*ListAppsParams contains all the parameters to send to the API endpoint +/* +ListAppsParams contains all the parameters to send to the API endpoint for the list apps operation typically these are written to a http.Request */ type ListAppsParams struct { @@ -71,6 +72,31 @@ type ListAppsParams struct { */ Name *string + /*DisplayName + OCI parity filter for display name. + + */ + DisplayName *string + /*ID + OCI parity filter for application OCID. + + */ + ID *string + /*LifecycleState + OCI parity filter for lifecycle state. + + */ + LifecycleState *string + /*SortBy + OCI parity sort field. + + */ + SortBy *string + /*SortOrder + OCI parity sort order. + + */ + SortOrder *string /*PerPage Number of results to return, defaults to 30. Max of 100. @@ -132,6 +158,12 @@ func (o *ListAppsParams) WithName(name *string) *ListAppsParams { return o } +func (o *ListAppsParams) GetDisplayName() *string { return o.DisplayName } +func (o *ListAppsParams) GetID() *string { return o.ID } +func (o *ListAppsParams) GetLifecycleState() *string { return o.LifecycleState } +func (o *ListAppsParams) GetSortBy() *string { return o.SortBy } +func (o *ListAppsParams) GetSortOrder() *string { return o.SortOrder } + // SetName adds the name to the list apps params func (o *ListAppsParams) SetName(name *string) { o.Name = name @@ -188,6 +220,66 @@ func (o *ListAppsParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Regi } + if o.DisplayName != nil { + var qrDisplayName string + if o.DisplayName != nil { + qrDisplayName = *o.DisplayName + } + if qrDisplayName != "" { + if err := r.SetQueryParam("display_name", qrDisplayName); err != nil { + return err + } + } + } + + if o.ID != nil { + var qrID string + if o.ID != nil { + qrID = *o.ID + } + if qrID != "" { + if err := r.SetQueryParam("id", qrID); err != nil { + return err + } + } + } + + if o.LifecycleState != nil { + var qrLifecycleState string + if o.LifecycleState != nil { + qrLifecycleState = *o.LifecycleState + } + if qrLifecycleState != "" { + if err := r.SetQueryParam("lifecycle_state", qrLifecycleState); err != nil { + return err + } + } + } + + if o.SortBy != nil { + var qrSortBy string + if o.SortBy != nil { + qrSortBy = *o.SortBy + } + if qrSortBy != "" { + if err := r.SetQueryParam("sort_by", qrSortBy); err != nil { + return err + } + } + } + + if o.SortOrder != nil { + var qrSortOrder string + if o.SortOrder != nil { + qrSortOrder = *o.SortOrder + } + if qrSortOrder != "" { + if err := r.SetQueryParam("sort_order", qrSortOrder); err != nil { + return err + } + } + } + if o.PerPage != nil { // query param per_page diff --git a/vendor/github.com/fnproject/fn_go/clientv2/apps/update_app_parameters.go b/vendor/github.com/fnproject/fn_go/clientv2/apps/update_app_parameters.go index bb466950..f8fb57aa 100644 --- a/vendor/github.com/fnproject/fn_go/clientv2/apps/update_app_parameters.go +++ b/vendor/github.com/fnproject/fn_go/clientv2/apps/update_app_parameters.go @@ -57,7 +57,8 @@ func NewUpdateAppParamsWithHTTPClient(client *http.Client) *UpdateAppParams { } } -/*UpdateAppParams contains all the parameters to send to the API endpoint +/* +UpdateAppParams contains all the parameters to send to the API endpoint for the update app operation typically these are written to a http.Request */ type UpdateAppParams struct { @@ -66,7 +67,11 @@ type UpdateAppParams struct { Opaque, unique Application ID. */ - AppID string + AppID string + IfMatch string + WaitForState string + MaxWaitSeconds int64 + WaitIntervalSeconds int64 /*Body Application data to merge with current values. diff --git a/vendor/github.com/fnproject/fn_go/clientv2/fns/create_fn_parameters.go b/vendor/github.com/fnproject/fn_go/clientv2/fns/create_fn_parameters.go index 8f12f014..e494e937 100644 --- a/vendor/github.com/fnproject/fn_go/clientv2/fns/create_fn_parameters.go +++ b/vendor/github.com/fnproject/fn_go/clientv2/fns/create_fn_parameters.go @@ -57,7 +57,8 @@ func NewCreateFnParamsWithHTTPClient(client *http.Client) *CreateFnParams { } } -/*CreateFnParams contains all the parameters to send to the API endpoint +/* +CreateFnParams contains all the parameters to send to the API endpoint for the create fn operation typically these are written to a http.Request */ type CreateFnParams struct { @@ -66,7 +67,10 @@ type CreateFnParams struct { Function data to insert. */ - Body *modelsv2.Fn + Body *modelsv2.Fn + WaitForState string + MaxWaitSeconds int64 + WaitIntervalSeconds int64 timeout time.Duration Context context.Context diff --git a/vendor/github.com/fnproject/fn_go/clientv2/fns/delete_fn_parameters.go b/vendor/github.com/fnproject/fn_go/clientv2/fns/delete_fn_parameters.go index a12806ba..007e844a 100644 --- a/vendor/github.com/fnproject/fn_go/clientv2/fns/delete_fn_parameters.go +++ b/vendor/github.com/fnproject/fn_go/clientv2/fns/delete_fn_parameters.go @@ -55,7 +55,8 @@ func NewDeleteFnParamsWithHTTPClient(client *http.Client) *DeleteFnParams { } } -/*DeleteFnParams contains all the parameters to send to the API endpoint +/* +DeleteFnParams contains all the parameters to send to the API endpoint for the delete fn operation typically these are written to a http.Request */ type DeleteFnParams struct { @@ -64,7 +65,11 @@ type DeleteFnParams struct { Opaque, unique Function ID. */ - FnID string + FnID string + IfMatch string + WaitForState string + MaxWaitSeconds int64 + WaitIntervalSeconds int64 timeout time.Duration Context context.Context diff --git a/vendor/github.com/fnproject/fn_go/clientv2/fns/list_fns_parameters.go b/vendor/github.com/fnproject/fn_go/clientv2/fns/list_fns_parameters.go index 37a47d0d..8362259d 100644 --- a/vendor/github.com/fnproject/fn_go/clientv2/fns/list_fns_parameters.go +++ b/vendor/github.com/fnproject/fn_go/clientv2/fns/list_fns_parameters.go @@ -56,7 +56,8 @@ func NewListFnsParamsWithHTTPClient(client *http.Client) *ListFnsParams { } } -/*ListFnsParams contains all the parameters to send to the API endpoint +/* +ListFnsParams contains all the parameters to send to the API endpoint for the list fns operation typically these are written to a http.Request */ type ListFnsParams struct { @@ -76,6 +77,31 @@ type ListFnsParams struct { */ Name *string + /*DisplayName + OCI parity filter for display name. + + */ + DisplayName *string + /*ID + OCI parity filter for function OCID. + + */ + ID *string + /*LifecycleState + OCI parity filter for lifecycle state. + + */ + LifecycleState *string + /*SortBy + OCI parity sort field. + + */ + SortBy *string + /*SortOrder + OCI parity sort order. + + */ + SortOrder *string /*PerPage Number of results to return, defaults to 30. Max of 100. @@ -153,6 +179,12 @@ func (o *ListFnsParams) SetName(name *string) { o.Name = name } +func (o *ListFnsParams) GetDisplayName() *string { return o.DisplayName } +func (o *ListFnsParams) GetID() *string { return o.ID } +func (o *ListFnsParams) GetLifecycleState() *string { return o.LifecycleState } +func (o *ListFnsParams) GetSortBy() *string { return o.SortBy } +func (o *ListFnsParams) GetSortOrder() *string { return o.SortOrder } + // WithPerPage adds the perPage to the list fns params func (o *ListFnsParams) WithPerPage(perPage *int64) *ListFnsParams { o.SetPerPage(perPage) @@ -220,6 +252,66 @@ func (o *ListFnsParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Regis } + if o.DisplayName != nil { + var qrDisplayName string + if o.DisplayName != nil { + qrDisplayName = *o.DisplayName + } + if qrDisplayName != "" { + if err := r.SetQueryParam("display_name", qrDisplayName); err != nil { + return err + } + } + } + + if o.ID != nil { + var qrID string + if o.ID != nil { + qrID = *o.ID + } + if qrID != "" { + if err := r.SetQueryParam("id", qrID); err != nil { + return err + } + } + } + + if o.LifecycleState != nil { + var qrLifecycleState string + if o.LifecycleState != nil { + qrLifecycleState = *o.LifecycleState + } + if qrLifecycleState != "" { + if err := r.SetQueryParam("lifecycle_state", qrLifecycleState); err != nil { + return err + } + } + } + + if o.SortBy != nil { + var qrSortBy string + if o.SortBy != nil { + qrSortBy = *o.SortBy + } + if qrSortBy != "" { + if err := r.SetQueryParam("sort_by", qrSortBy); err != nil { + return err + } + } + } + + if o.SortOrder != nil { + var qrSortOrder string + if o.SortOrder != nil { + qrSortOrder = *o.SortOrder + } + if qrSortOrder != "" { + if err := r.SetQueryParam("sort_order", qrSortOrder); err != nil { + return err + } + } + } + if o.PerPage != nil { // query param per_page diff --git a/vendor/github.com/fnproject/fn_go/clientv2/fns/update_fn_parameters.go b/vendor/github.com/fnproject/fn_go/clientv2/fns/update_fn_parameters.go index d3c30261..f17f2cbb 100644 --- a/vendor/github.com/fnproject/fn_go/clientv2/fns/update_fn_parameters.go +++ b/vendor/github.com/fnproject/fn_go/clientv2/fns/update_fn_parameters.go @@ -57,7 +57,8 @@ func NewUpdateFnParamsWithHTTPClient(client *http.Client) *UpdateFnParams { } } -/*UpdateFnParams contains all the parameters to send to the API endpoint +/* +UpdateFnParams contains all the parameters to send to the API endpoint for the update fn operation typically these are written to a http.Request */ type UpdateFnParams struct { @@ -66,7 +67,11 @@ type UpdateFnParams struct { Function data to merge with current values. */ - Body *modelsv2.Fn + Body *modelsv2.Fn + IfMatch string + WaitForState string + MaxWaitSeconds int64 + WaitIntervalSeconds int64 /*FnID Opaque, unique Function ID. diff --git a/vendor/github.com/fnproject/fn_go/provider/oracle/shim/apps.go b/vendor/github.com/fnproject/fn_go/provider/oracle/shim/apps.go index 5520ff44..6287f815 100644 --- a/vendor/github.com/fnproject/fn_go/provider/oracle/shim/apps.go +++ b/vendor/github.com/fnproject/fn_go/provider/oracle/shim/apps.go @@ -55,6 +55,9 @@ func (s *appsShim) CreateApp(params *apps.CreateAppParams) (*apps.CreateAppOK, e SyslogUrl: params.Body.SyslogURL, Shape: shape, } + if err := applyGeneratedOCIParityCreateApplicationDetails(&details, params.Body.Annotations); err != nil { + return nil, err + } req := functions.CreateApplicationRequest{CreateApplicationDetails: details} @@ -105,6 +108,7 @@ func (s *appsShim) ListApps(params *apps.ListAppsParams) (*apps.ListAppsOK, erro Page: params.Cursor, DisplayName: params.Name, } + applyGeneratedOCIParityListApplicationsRequest(params, &req) var applicationSummaries []functions.ApplicationSummary @@ -183,11 +187,14 @@ func (s *appsShim) UpdateApp(params *apps.UpdateAppParams) (*apps.UpdateAppOK, e DefinedTags: definedTags, SyslogUrl: params.Body.SyslogURL, } + if err := applyGeneratedOCIParityUpdateApplicationDetails(&details, params.Body.Annotations); err != nil { + return nil, err + } req := functions.UpdateApplicationRequest{ ApplicationId: ¶ms.AppID, UpdateApplicationDetails: details, - IfMatch: etag, + IfMatch: stringPtrOr(params.IfMatch, etag), } res, err := s.ociClient.UpdateApplication(ctxOrBackground(params.Context), req) 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 b87bd9e1..9a4b2f13 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 @@ -85,6 +85,9 @@ func (s *fnsShim) CreateFn(params *fns.CreateFnParams) (*fns.CreateFnOK, error) DefinedTags: definedTags, TimeoutInSeconds: parseTimeout(params.Body.Timeout), } + if err := applyGeneratedOCIParityCreateFunctionDetails(&details, params.Body.Annotations); err != nil { + return nil, err + } if detachedTimeoutSeconds, err := parseDetachedTimeoutAnnotation(params.Body.Annotations); err != nil { return nil, err } else if detachedTimeoutSeconds != nil { @@ -93,8 +96,12 @@ func (s *fnsShim) CreateFn(params *fns.CreateFnParams) (*fns.CreateFnOK, error) if successDestination, failureDestination, err := parseDestinationAnnotations(params.Body.Annotations); err != nil { return nil, err } else { - details.SuccessDestination = successDestination - details.FailureDestination = failureDestination + if successDestination != nil { + details.SuccessDestination = successDestination + } + if failureDestination != nil { + details.FailureDestination = failureDestination + } } req := functions.CreateFunctionRequest{CreateFunctionDetails: details} @@ -111,6 +118,7 @@ func (s *fnsShim) CreateFn(params *fns.CreateFnParams) (*fns.CreateFnOK, error) func (s *fnsShim) DeleteFn(params *fns.DeleteFnParams) (*fns.DeleteFnNoContent, error) { req := functions.DeleteFunctionRequest{FunctionId: ¶ms.FnID} + req.IfMatch = stringPtr(params.IfMatch) _, err := s.ociClient.DeleteFunction(ctxOrBackground(params.Context), req) if err != nil { @@ -146,6 +154,7 @@ func (s *fnsShim) ListFns(params *fns.ListFnsParams) (*fns.ListFnsOK, error) { Page: params.Cursor, DisplayName: params.Name, } + applyGeneratedOCIParityListFunctionsRequest(params, &req) var functionSummaries []functions.FunctionSummary @@ -243,6 +252,9 @@ func (s *fnsShim) UpdateFn(params *fns.UpdateFnParams) (*fns.UpdateFnOK, error) DefinedTags: definedTags, TimeoutInSeconds: parseTimeout(params.Body.Timeout), } + if err := applyGeneratedOCIParityUpdateFunctionDetails(&details, params.Body.Annotations); err != nil { + return nil, err + } if detachedTimeoutSeconds, err := parseDetachedTimeoutAnnotation(params.Body.Annotations); err != nil { return nil, err } else if detachedTimeoutSeconds != nil { @@ -251,14 +263,18 @@ func (s *fnsShim) UpdateFn(params *fns.UpdateFnParams) (*fns.UpdateFnOK, error) if successDestination, failureDestination, err := parseDestinationAnnotations(params.Body.Annotations); err != nil { return nil, err } else { - details.SuccessDestination = successDestination - details.FailureDestination = failureDestination + if successDestination != nil { + details.SuccessDestination = successDestination + } + if failureDestination != nil { + details.FailureDestination = failureDestination + } } req := functions.UpdateFunctionRequest{ FunctionId: ¶ms.FnID, UpdateFunctionDetails: details, - IfMatch: etag, + IfMatch: stringPtrOr(params.IfMatch, etag), } res, err := s.ociClient.UpdateFunction(ctxOrBackground(params.Context), req) @@ -523,6 +539,7 @@ func ociFnToV2(ociFn functions.Function) *modelsv2.Fn { addProvisionedConcurrencyAnnotations(annotations, ociFn.ProvisionedConcurrencyConfig) addTagAnnotations(annotations, ociFn.FreeformTags, ociFn.DefinedTags) addSourceDetailsAnnotations(annotations, ociFn.SourceDetails) + addTraceConfigAnnotation(annotations, ociFn.TraceConfig) if ociFn.DetachedModeTimeoutInSeconds != nil { annotations[annotationDetachedTimeoutSeconds] = *ociFn.DetachedModeTimeoutInSeconds } @@ -570,6 +587,7 @@ func ociFnSummaryToV2(ociFnSummary functions.FunctionSummary) *modelsv2.Fn { addProvisionedConcurrencyAnnotations(annotations, ociFnSummary.ProvisionedConcurrencyConfig) addTagAnnotations(annotations, ociFnSummary.FreeformTags, ociFnSummary.DefinedTags) addSourceDetailsAnnotations(annotations, ociFnSummary.SourceDetails) + addTraceConfigAnnotation(annotations, ociFnSummary.TraceConfig) if ociFnSummary.DetachedModeTimeoutInSeconds != nil { annotations[annotationDetachedTimeoutSeconds] = *ociFnSummary.DetachedModeTimeoutInSeconds } @@ -651,3 +669,16 @@ func addSourceDetailsAnnotations(annotations map[string]interface{}, sourceDetai } } } + +func addTraceConfigAnnotation(annotations map[string]interface{}, traceConfig *functions.FunctionTraceConfig) { + if annotations == nil || traceConfig == nil { + return + } + trace := map[string]interface{}{} + if traceConfig.IsEnabled != nil { + trace["isEnabled"] = *traceConfig.IsEnabled + } + if len(trace) > 0 { + annotations[annotationOCIParityFnTraceConfig] = trace + } +} diff --git a/vendor/github.com/fnproject/fn_go/provider/oracle/shim/generated_app_oci_parity.go b/vendor/github.com/fnproject/fn_go/provider/oracle/shim/generated_app_oci_parity.go new file mode 100644 index 00000000..25b0929f --- /dev/null +++ b/vendor/github.com/fnproject/fn_go/provider/oracle/shim/generated_app_oci_parity.go @@ -0,0 +1,147 @@ +// Code generated by make generate-oci-parity; DO NOT EDIT. + +package shim + +import ( + "encoding/json" + "fmt" + + "github.com/oracle/oci-go-sdk/v65/functions" +) + +const ( + annotationOCIParityImagePolicyConfig = "oracle.com/oci/parity/imagePolicyConfig" + annotationOCIParityNetworkSecurityGroupIds = "oracle.com/oci/parity/networkSecurityGroupIds" + annotationOCIParitySecurityAttributes = "oracle.com/oci/parity/securityAttributes" + annotationOCIParityTraceConfig = "oracle.com/oci/parity/traceConfig" +) + +func applyGeneratedOCIParityCreateApplicationDetails(details *functions.CreateApplicationDetails, annotations map[string]interface{}) error { + if raw, ok := annotations[annotationOCIParityImagePolicyConfig]; ok { + data, err := json.Marshal(raw) + if err != nil { + return err + } + var parsed functions.ImagePolicyConfig + if err := json.Unmarshal(data, &parsed); err != nil { + return fmt.Errorf("invalid imagePolicyConfig parity annotation: %w", err) + } + details.ImagePolicyConfig = &parsed + } + if raw, ok := annotations[annotationOCIParityNetworkSecurityGroupIds]; ok { + var ids []string + switch typed := raw.(type) { + case []interface{}: + for _, v := range typed { + if s, ok := v.(string); ok { + ids = append(ids, s) + } + } + case []string: + ids = append(ids, typed...) + default: + return fmt.Errorf("invalid networkSecurityGroupIds parity annotation") + } + details.NetworkSecurityGroupIds = ids + } + if raw, ok := annotations[annotationOCIParitySecurityAttributes]; ok { + data, err := json.Marshal(raw) + if err != nil { + return err + } + var parsed map[string]map[string]interface{} + if err := json.Unmarshal(data, &parsed); err != nil { + return fmt.Errorf("invalid securityAttributes parity annotation: %w", err) + } + details.SecurityAttributes = parsed + } + if raw, ok := annotations[annotationOCIParityTraceConfig]; ok { + data, err := json.Marshal(raw) + if err != nil { + return err + } + var parsed functions.ApplicationTraceConfig + if err := json.Unmarshal(data, &parsed); err != nil { + return fmt.Errorf("invalid traceConfig parity annotation: %w", err) + } + details.TraceConfig = &parsed + } + return nil +} + +func applyGeneratedOCIParityUpdateApplicationDetails(details *functions.UpdateApplicationDetails, annotations map[string]interface{}) error { + if raw, ok := annotations[annotationOCIParityImagePolicyConfig]; ok { + data, err := json.Marshal(raw) + if err != nil { + return err + } + var parsed functions.ImagePolicyConfig + if err := json.Unmarshal(data, &parsed); err != nil { + return fmt.Errorf("invalid imagePolicyConfig parity annotation: %w", err) + } + details.ImagePolicyConfig = &parsed + } + if raw, ok := annotations[annotationOCIParityNetworkSecurityGroupIds]; ok { + var ids []string + switch typed := raw.(type) { + case []interface{}: + for _, v := range typed { + if s, ok := v.(string); ok { + ids = append(ids, s) + } + } + case []string: + ids = append(ids, typed...) + default: + return fmt.Errorf("invalid networkSecurityGroupIds parity annotation") + } + details.NetworkSecurityGroupIds = ids + } + if raw, ok := annotations[annotationOCIParitySecurityAttributes]; ok { + data, err := json.Marshal(raw) + if err != nil { + return err + } + var parsed map[string]map[string]interface{} + if err := json.Unmarshal(data, &parsed); err != nil { + return fmt.Errorf("invalid securityAttributes parity annotation: %w", err) + } + details.SecurityAttributes = parsed + } + if raw, ok := annotations[annotationOCIParityTraceConfig]; ok { + data, err := json.Marshal(raw) + if err != nil { + return err + } + var parsed functions.ApplicationTraceConfig + if err := json.Unmarshal(data, &parsed); err != nil { + return fmt.Errorf("invalid traceConfig parity annotation: %w", err) + } + details.TraceConfig = &parsed + } + return nil +} + +func applyGeneratedOCIParityListApplicationsRequest(params interface { + GetDisplayName() *string + GetID() *string + GetLifecycleState() *string + GetSortBy() *string + GetSortOrder() *string +}, req *functions.ListApplicationsRequest) { + if params.GetDisplayName() != nil { + req.DisplayName = params.GetDisplayName() + } + if params.GetID() != nil { + req.Id = params.GetID() + } + if params.GetLifecycleState() != nil { + req.LifecycleState = functions.ApplicationLifecycleStateEnum(*params.GetLifecycleState()) + } + if params.GetSortBy() != nil { + req.SortBy = functions.ListApplicationsSortByEnum(*params.GetSortBy()) + } + if params.GetSortOrder() != nil { + req.SortOrder = functions.ListApplicationsSortOrderEnum(*params.GetSortOrder()) + } +} diff --git a/vendor/github.com/fnproject/fn_go/provider/oracle/shim/generated_fn_oci_parity.go b/vendor/github.com/fnproject/fn_go/provider/oracle/shim/generated_fn_oci_parity.go new file mode 100644 index 00000000..0ec9ec29 --- /dev/null +++ b/vendor/github.com/fnproject/fn_go/provider/oracle/shim/generated_fn_oci_parity.go @@ -0,0 +1,233 @@ +// Code generated by make generate-oci-parity; DO NOT EDIT. + +package shim + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/oracle/oci-go-sdk/v65/functions" +) + +const ( + annotationOCIParityFnDetachedModeTimeoutInSeconds = "oracle.com/oci/parity/detachedModeTimeoutInSeconds" + annotationOCIParityFnFailureDestination = "oracle.com/oci/parity/failureDestination" + annotationOCIParityFnSuccessDestination = "oracle.com/oci/parity/successDestination" + annotationOCIParityFnTraceConfig = "oracle.com/oci/parity/traceConfig" +) + +func parseGeneratedSuccessDestination(raw interface{}) (functions.SuccessDestinationDetails, error) { + data, err := json.Marshal(raw) + if err != nil { + return nil, err + } + var kindHolder map[string]interface{} + if err := json.Unmarshal(data, &kindHolder); err != nil { + return nil, err + } + kind, _ := kindHolder["kind"].(string) + kind = strings.ToUpper(strings.TrimSpace(kind)) + if kind == "NOTIFICATIONS" { + kind = "NOTIFICATION" + kindHolder["kind"] = kind + data, err = json.Marshal(kindHolder) + if err != nil { + return nil, err + } + } + switch kind { + case "STREAM": + var parsed functions.StreamSuccessDestinationDetails + if err := json.Unmarshal(data, &parsed); err != nil { + return nil, err + } + return parsed, nil + case "QUEUE": + var parsed functions.QueueSuccessDestinationDetails + if err := json.Unmarshal(data, &parsed); err != nil { + return nil, err + } + return parsed, nil + case "NOTIFICATION": + var parsed functions.NotificationSuccessDestinationDetails + if err := json.Unmarshal(data, &parsed); err != nil { + return nil, err + } + return parsed, nil + case "NONE": + var parsed functions.NoneSuccessDestinationDetails + if err := json.Unmarshal(data, &parsed); err != nil { + return nil, err + } + return parsed, nil + default: + return nil, fmt.Errorf("invalid successDestination parity annotation kind %q", kind) + } +} + +func parseGeneratedFailureDestination(raw interface{}) (functions.FailureDestinationDetails, error) { + data, err := json.Marshal(raw) + if err != nil { + return nil, err + } + var kindHolder map[string]interface{} + if err := json.Unmarshal(data, &kindHolder); err != nil { + return nil, err + } + kind, _ := kindHolder["kind"].(string) + kind = strings.ToUpper(strings.TrimSpace(kind)) + if kind == "NOTIFICATIONS" { + kind = "NOTIFICATION" + kindHolder["kind"] = kind + data, err = json.Marshal(kindHolder) + if err != nil { + return nil, err + } + } + switch kind { + case "STREAM": + var parsed functions.StreamFailureDestinationDetails + if err := json.Unmarshal(data, &parsed); err != nil { + return nil, err + } + return parsed, nil + case "QUEUE": + var parsed functions.QueueFailureDestinationDetails + if err := json.Unmarshal(data, &parsed); err != nil { + return nil, err + } + return parsed, nil + case "NOTIFICATION": + var parsed functions.NotificationFailureDestinationDetails + if err := json.Unmarshal(data, &parsed); err != nil { + return nil, err + } + return parsed, nil + case "NONE": + var parsed functions.NoneFailureDestinationDetails + if err := json.Unmarshal(data, &parsed); err != nil { + return nil, err + } + return parsed, nil + default: + return nil, fmt.Errorf("invalid failureDestination parity annotation kind %q", kind) + } +} +func applyGeneratedOCIParityCreateFunctionDetails(details *functions.CreateFunctionDetails, annotations map[string]interface{}) error { + if raw, ok := annotations[annotationOCIParityFnDetachedModeTimeoutInSeconds]; ok { + switch typed := raw.(type) { + case int: + v := typed + details.DetachedModeTimeoutInSeconds = &v + case int32: + v := int(typed) + details.DetachedModeTimeoutInSeconds = &v + case int64: + v := int(typed) + details.DetachedModeTimeoutInSeconds = &v + case float64: + v := int(typed) + details.DetachedModeTimeoutInSeconds = &v + default: + return fmt.Errorf("invalid detachedModeTimeoutInSeconds parity annotation") + } + } + if raw, ok := annotations[annotationOCIParityFnFailureDestination]; ok { + parsed, err := parseGeneratedFailureDestination(raw) + if err != nil { + return fmt.Errorf("invalid failureDestination parity annotation: %w", err) + } + details.FailureDestination = parsed + } + if raw, ok := annotations[annotationOCIParityFnSuccessDestination]; ok { + parsed, err := parseGeneratedSuccessDestination(raw) + if err != nil { + return fmt.Errorf("invalid successDestination parity annotation: %w", err) + } + details.SuccessDestination = parsed + } + if raw, ok := annotations[annotationOCIParityFnTraceConfig]; ok { + data, err := json.Marshal(raw) + if err != nil { + return err + } + var parsed functions.FunctionTraceConfig + if err := json.Unmarshal(data, &parsed); err != nil { + return fmt.Errorf("invalid traceConfig parity annotation: %w", err) + } + details.TraceConfig = &parsed + } + return nil +} + +func applyGeneratedOCIParityUpdateFunctionDetails(details *functions.UpdateFunctionDetails, annotations map[string]interface{}) error { + if raw, ok := annotations[annotationOCIParityFnDetachedModeTimeoutInSeconds]; ok { + switch typed := raw.(type) { + case int: + v := typed + details.DetachedModeTimeoutInSeconds = &v + case int32: + v := int(typed) + details.DetachedModeTimeoutInSeconds = &v + case int64: + v := int(typed) + details.DetachedModeTimeoutInSeconds = &v + case float64: + v := int(typed) + details.DetachedModeTimeoutInSeconds = &v + default: + return fmt.Errorf("invalid detachedModeTimeoutInSeconds parity annotation") + } + } + if raw, ok := annotations[annotationOCIParityFnFailureDestination]; ok { + parsed, err := parseGeneratedFailureDestination(raw) + if err != nil { + return fmt.Errorf("invalid failureDestination parity annotation: %w", err) + } + details.FailureDestination = parsed + } + if raw, ok := annotations[annotationOCIParityFnSuccessDestination]; ok { + parsed, err := parseGeneratedSuccessDestination(raw) + if err != nil { + return fmt.Errorf("invalid successDestination parity annotation: %w", err) + } + details.SuccessDestination = parsed + } + if raw, ok := annotations[annotationOCIParityFnTraceConfig]; ok { + data, err := json.Marshal(raw) + if err != nil { + return err + } + var parsed functions.FunctionTraceConfig + if err := json.Unmarshal(data, &parsed); err != nil { + return fmt.Errorf("invalid traceConfig parity annotation: %w", err) + } + details.TraceConfig = &parsed + } + return nil +} + +func applyGeneratedOCIParityListFunctionsRequest(params interface { + GetDisplayName() *string + GetID() *string + GetLifecycleState() *string + GetSortBy() *string + GetSortOrder() *string +}, req *functions.ListFunctionsRequest) { + if params.GetDisplayName() != nil { + req.DisplayName = params.GetDisplayName() + } + if params.GetID() != nil { + req.Id = params.GetID() + } + if params.GetLifecycleState() != nil { + req.LifecycleState = functions.FunctionLifecycleStateEnum(*params.GetLifecycleState()) + } + if params.GetSortBy() != nil { + req.SortBy = functions.ListFunctionsSortByEnum(*params.GetSortBy()) + } + if params.GetSortOrder() != nil { + req.SortOrder = functions.ListFunctionsSortOrderEnum(*params.GetSortOrder()) + } +} diff --git a/vendor/github.com/fnproject/fn_go/provider/oracle/shim/request_control.go b/vendor/github.com/fnproject/fn_go/provider/oracle/shim/request_control.go new file mode 100644 index 00000000..dba5f54a --- /dev/null +++ b/vendor/github.com/fnproject/fn_go/provider/oracle/shim/request_control.go @@ -0,0 +1,15 @@ +package shim + +func stringPtr(value string) *string { + if value == "" { + return nil + } + return &value +} + +func stringPtrOr(value string, fallback *string) *string { + if value != "" { + return &value + } + return fallback +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 2c02a7ca..2b466666 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -16,8 +16,8 @@ github.com/davecgh/go-spew/spew # github.com/fatih/color v0.0.0-20170926111411-5df930a27be2 ## explicit github.com/fatih/color -# github.com/fnproject/fn_go v0.8.10 -## explicit; go 1.17 +# github.com/fnproject/fn_go v0.8.11 +## explicit; go 1.24.0 github.com/fnproject/fn_go github.com/fnproject/fn_go/client/version github.com/fnproject/fn_go/clientv2