From 4dfefb16f87dd8050b16a677dc981e7e1090e678 Mon Sep 17 00:00:00 2001 From: jayarora Date: Tue, 21 Apr 2026 19:43:41 +0100 Subject: [PATCH 1/2] Add code-only push support for Object Storage archives --- commands/push.go | 99 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/commands/push.go b/commands/push.go index 0a3efc8d..3ac48800 100644 --- a/commands/push.go +++ b/commands/push.go @@ -17,10 +17,20 @@ package commands import ( + "context" "errors" "fmt" + "net/url" + "os" + "path/filepath" + "strings" + "github.com/fnproject/cli/client" "github.com/fnproject/cli/common" + "github.com/fnproject/cli/config" + fnprovider "github.com/fnproject/fn_go/provider/oracle" + "github.com/oracle/oci-go-sdk/v65/objectstorage" + "github.com/spf13/viper" "github.com/urfave/cli" ) @@ -75,6 +85,15 @@ func (p *pushcmd) push(c *cli.Context) error { return err } + if ff.Code_only { + objectName, err := pushCodeOnlyArchive(ff) + if err != nil { + return err + } + fmt.Printf("Code-only archive uploaded successfully as %s\n", objectName) + return nil + } + fmt.Println("pushing", ff.ImageNameV20180708()) if err := common.PushV20180708(ff); err != nil { @@ -103,3 +122,83 @@ func (p *pushcmd) push(c *cli.Context) error { fmt.Printf("Function %v pushed successfully to the registry.\n", ff.ImageName()) return nil } + +func pushCodeOnlyArchive(ff *common.FuncFileV20180708) (string, error) { + contextName := viper.GetString(config.CurrentContext) + contextPath := filepath.Join(config.GetHomeDir(), ".fn", "contexts", contextName+".yaml") + ctxFile, err := config.NewContextFile(contextPath) + if err != nil { + return "", err + } + if strings.TrimSpace(ctxFile.ObjectStorageBucketName) == "" || strings.TrimSpace(ctxFile.ObjectStorageNamespace) == "" { + return "", errors.New("code-only Object Storage target is not configured in the current context") + } + archivePath := fmt.Sprintf("%s.%s.zip", ff.Name, ff.Version) + if _, err := os.Stat(archivePath); err != nil { + return "", fmt.Errorf("built archive not found at %s: %w", archivePath, err) + } + provider, err := client.CurrentProvider() + if err != nil { + return "", err + } + ociProvider, ok := provider.(*fnprovider.OracleProvider) + if !ok || ociProvider == nil { + return "", errors.New("code-only archive push requires an oracle provider") + } + client, err := objectstorage.NewObjectStorageClientWithConfigurationProvider(ociProvider.ConfigurationProvider) + if err != nil { + return "", err + } + region, err := ociProvider.ConfigurationProvider.Region() + if err == nil { + client.SetRegion(region) + } + if client.Host == "" || strings.Contains(client.Host, "objectstorage..") { + if ociProvider.FnApiUrl != nil { + objectStorageHost, hostErr := objectStorageHostFromFnAPIURL(ociProvider.FnApiUrl) + if hostErr != nil { + return "", hostErr + } + client.Host = objectStorageHost + } + } + file, err := os.Open(archivePath) + if err != nil { + return "", err + } + defer file.Close() + info, err := file.Stat() + if err != nil { + return "", err + } + objectName := archivePath + contentLength := info.Size() + request := objectstorage.PutObjectRequest{ + NamespaceName: &ctxFile.ObjectStorageNamespace, + BucketName: &ctxFile.ObjectStorageBucketName, + ObjectName: &objectName, + PutObjectBody: file, + ContentLength: &contentLength, + } + _, err = client.PutObject(context.Background(), request) + if err != nil { + return "", fmt.Errorf("failed to upload archive to Object Storage: %w", err) + } + return objectName, nil +} + +func objectStorageHostFromFnAPIURL(fnAPIURL *url.URL) (string, error) { + if fnAPIURL == nil { + return "", errors.New("unable to derive Object Storage host from nil Functions API URL") + } + hostParts := strings.Split(fnAPIURL.Host, ".") + if len(hostParts) < 4 { + return "", fmt.Errorf("unable to derive Object Storage host from Functions API host %s", fnAPIURL.Host) + } + region := hostParts[1] + domain := strings.Join(hostParts[3:], ".") + if region == "" || domain == "" { + return "", fmt.Errorf("unable to derive Object Storage host from Functions API host %s", fnAPIURL.Host) + } + return fmt.Sprintf("https://objectstorage.%s.%s", region, domain), nil +} From 6c0a138adafb6da346b6126c36cc893b78153784 Mon Sep 17 00:00:00 2001 From: jayarora Date: Thu, 23 Apr 2026 12:16:16 +0100 Subject: [PATCH 2/2] Add code-only push tests --- test/cli_code_only_push_test.go | 52 +++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 test/cli_code_only_push_test.go diff --git a/test/cli_code_only_push_test.go b/test/cli_code_only_push_test.go new file mode 100644 index 00000000..2415e8a1 --- /dev/null +++ b/test/cli_code_only_push_test.go @@ -0,0 +1,52 @@ +package test + +import ( + "fmt" + "testing" + + "github.com/fnproject/cli/common" + "github.com/fnproject/cli/testharness" +) + +func TestCodeOnlyPushValidation(t *testing.T) { + t.Run("code-only push should fail when Object Storage target is not configured in current context", func(t *testing.T) { + t.Parallel() + h := testharness.Create(t) + defer h.Cleanup() + + appName := h.NewAppName() + funcName := h.NewFuncName(appName) + dirName := funcName + "_dir" + h.Fn("init", "--code-only", "--runtime-name", "python311.ol9", "--runtime-config-type", "function-update", "--name", funcName, dirName).AssertSuccess() + + h.Cd(dirName) + h.Fn("build").AssertSuccess() + h.Fn("push").AssertFailed().AssertStderrContains("code-only Object Storage target is not configured in the current context") + }) + + t.Run("code-only push should fail when built archive is missing even if context has bucket and namespace", func(t *testing.T) { + t.Parallel() + h := testharness.Create(t) + defer h.Cleanup() + + contextName := h.NewContextName() + h.Fn("create", "context", "--api-url", "http://localhost:8080", contextName).AssertSuccess() + h.Fn("use", "context", contextName).AssertSuccess() + h.Fn("update", "context", "object_storage_bucket_name", "code-only-test-files").AssertSuccess() + h.Fn("update", "context", "namespace", "oraclefunctionsdevelopm").AssertSuccess() + + h.MkDir("hello") + h.Cd("hello") + h.WithFile("func.yaml", fmt.Sprintf(`schema_version: %d +name: hello +version: 0.0.1 +code_only: true +runtime_config: + type: function-update + runtime_name: python311.ol9 +handler: hello_world.handler +`, common.LatestYamlVersion), 0644) + + h.Fn("push").AssertFailed().AssertStderrContains("built archive not found at hello.0.0.1.zip") + }) +} \ No newline at end of file