From 3fcbf53a66ada9a580c21bbabcd7ebf359e2d5a6 Mon Sep 17 00:00:00 2001 From: David Colburn Date: Mon, 21 Sep 2026 21:25:41 -0400 Subject: [PATCH 1/2] Add a native OCI Object Storage backend OCI is reachable today only through s3.go as an S3-compatible endpoint, which signs with SigV4 against a static Customer Secret Key and cannot use an OKE workload principal. The native API can, so add a backend that speaks it directly. Credentials come from workload identity, explicit API-key fields, or the SDK's default chain, in that order. The flag defaults to off, so nothing changes for existing consumers. GeneratePresignedUrl maps onto a pre-authenticated request, which differs from an S3 presigned URL in ways the method documents. --- config.go | 3 + config/config.go | 18 +++ error_test.go | 35 ++++++ go.mod | 4 + go.sum | 88 ++++++++++++++ location_test.go | 66 +++++++++++ oci.go | 302 +++++++++++++++++++++++++++++++++++++++++++++++ oci_test.go | 65 ++++++++++ storage_test.go | 28 +++++ 9 files changed, 609 insertions(+) create mode 100644 oci.go create mode 100644 oci_test.go diff --git a/config.go b/config.go index 9ec3a55..cd10d85 100644 --- a/config.go +++ b/config.go @@ -29,6 +29,7 @@ type ( AzureConfig = config.AzureConfig GCPConfig = config.GCPConfig LocalConfig = config.LocalConfig + OCIConfig = config.OCIConfig S3Config = config.S3Config ProxyConfig = config.ProxyConfig ) @@ -43,6 +44,8 @@ func newStorage(conf Config) (Storage, error) { return NewGCP(c) case *LocalConfig: return NewLocal(c) + case *OCIConfig: + return NewOCI(c) case *S3Config: return NewS3(c) default: diff --git a/config/config.go b/config/config.go index e04f139..e710fd0 100644 --- a/config/config.go +++ b/config/config.go @@ -59,6 +59,24 @@ type LocalConfig struct { func (*LocalConfig) storageConfig() {} +type OCIConfig struct { + TenancyOCID string `yaml:"tenancy_ocid,omitempty"` + UserOCID string `yaml:"user_ocid,omitempty"` + Fingerprint string `yaml:"fingerprint,omitempty"` + PrivateKey string `yaml:"private_key,omitempty"` // PEM contents, not a path + PrivateKeyPassphrase string `yaml:"private_key_passphrase,omitempty"` + + UseWorkloadIdentity bool `yaml:"use_workload_identity,omitempty"` + + Region string `yaml:"region,omitempty"` + Namespace string `yaml:"namespace,omitempty"` // discovered via GetNamespace when empty + CompartmentID string `yaml:"compartment_id,omitempty"` // only used for cross-tenancy namespace lookup + Bucket string `yaml:"bucket,omitempty"` + Endpoint string `yaml:"endpoint,omitempty"` // overrides the derived regional endpoint +} + +func (*OCIConfig) storageConfig() {} + type S3Config struct { AccessKey string `yaml:"access_key,omitempty"` Secret string `yaml:"secret,omitempty"` diff --git a/error_test.go b/error_test.go index 9649c94..3f0966b 100644 --- a/error_test.go +++ b/error_test.go @@ -200,3 +200,38 @@ func TestWrapAliOSSError(t *testing.T) { require.False(t, errors.As(got, &sce)) }) } + +// ociServiceError stands in for the SDK's unexported servicefailure, which +// implements common.ServiceError. +type ociServiceError struct{ status int } + +func (e *ociServiceError) Error() string { return "oci failure" } +func (e *ociServiceError) GetHTTPStatusCode() int { return e.status } +func (e *ociServiceError) GetMessage() string { return "oci failure" } +func (e *ociServiceError) GetCode() string { return "ObjectNotFound" } +func (e *ociServiceError) GetOpcRequestID() string { return "req-1" } + +func TestWrapOCIError(t *testing.T) { + t.Run("nil", func(t *testing.T) { + require.NoError(t, wrapOCIError(nil)) + }) + + t.Run("ServiceError", func(t *testing.T) { + inner := &ociServiceError{status: 403} + requireStatus(t, wrapOCIError(inner), 403, inner) + }) + + t.Run("wrapped ServiceError", func(t *testing.T) { + inner := &ociServiceError{status: 404} + wrapped := fmt.Errorf("get: %w", inner) + requireStatus(t, wrapOCIError(wrapped), 404, inner) + }) + + t.Run("plain error passes through", func(t *testing.T) { + plain := errors.New("eof") + got := wrapOCIError(plain) + require.Same(t, plain, got) + var sce *ErrorWithStatusCode + require.False(t, errors.As(got, &sce)) + }) +} diff --git a/go.mod b/go.mod index 093fc2a..470be5d 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/googleapis/gax-go/v2 v2.22.0 github.com/joho/godotenv v1.5.1 github.com/livekit/protocol v1.46.7-0.20260605212020-c0615a2f6f84 + github.com/oracle/oci-go-sdk/v65 v65.100.0 github.com/stretchr/testify v1.11.1 golang.org/x/oauth2 v0.36.0 google.golang.org/api v0.283.0 @@ -55,6 +56,7 @@ require ( github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/gofrs/flock v0.8.1 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.16 // indirect @@ -62,7 +64,9 @@ require ( github.com/planetscale/vtprotobuf v0.6.1-0.20250313105119-ba97887b0a25 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/puzpuzpuz/xsync/v4 v4.5.0 // indirect + github.com/sony/gobreaker v0.5.0 // indirect github.com/spiffe/go-spiffe/v2 v2.7.0 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/zeebo/xxh3 v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect diff --git a/go.sum b/go.sum index 7bc4eec..42bd4d9 100644 --- a/go.sum +++ b/go.sum @@ -92,6 +92,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dennwc/iters v1.2.2 h1:XH2/Etihiy9ZvPOVCR+icQXeYlhbvS7k0qro4x/2qQo= @@ -115,12 +117,15 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/cel-go v0.28.1 h1:YWIwi77J4xIsYUwAF/iIuS6haffzIHS8yWI8glSbLWM= github.com/google/cel-go v0.28.1/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= @@ -157,10 +162,13 @@ github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/oracle/oci-go-sdk/v65 v65.100.0 h1:pORdvqim3VW6X6g/Esi7/raCzDHpQ4BuREOoj5TS6bg= +github.com/oracle/oci-go-sdk/v65 v65.100.0/go.mod h1:RGiXfpDDmRRlLtqlStTzeBjjdUNXyqm3KXKyLCm3A/Q= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/planetscale/vtprotobuf v0.6.1-0.20250313105119-ba97887b0a25 h1:S1hI5JiKP7883xBzZAr1ydcxrKNSVNm7+3+JwjxZEsg= github.com/planetscale/vtprotobuf v0.6.1-0.20250313105119-ba97887b0a25/go.mod h1:ZQntvDG8TkPgljxtA0R9frDoND4QORU1VXz015N5Ks4= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/puzpuzpuz/xsync/v4 v4.5.0 h1:vOSWu6b57/emh+L/Cw0BeQfvxa/cogFywXHeGUxQxAg= @@ -169,12 +177,26 @@ github.com/redis/go-redis/v9 v9.20.0 h1:WnQYxLkgO2xiXTCJY0ldIiI8dNqCDlQAG+AtaH7a github.com/redis/go-redis/v9 v9.20.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= +github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spiffe/go-spiffe/v2 v2.7.0 h1:uXe1MflJoHw58wAUvxVlcM7WpKtijWG7I1UidcGh6g4= github.com/spiffe/go-spiffe/v2 v2.7.0/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJXP61mNV3/7iuU= github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= @@ -213,22 +235,87 @@ go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20260603202125-055de637280b h1:v1uXiEBHo8QA0LiGCo7UgHMzHT4Kdfpl2zmtH5vaP1Q= golang.org/x/exp v0.0.0-20260603202125-055de637280b/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/api v0.283.0 h1:0lkp8u0MPwJVHqRL+nJlMAoZVVzbmiXmFHXMOTmSPik= @@ -246,5 +333,6 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/location_test.go b/location_test.go index b390b79..6ea3646 100644 --- a/location_test.go +++ b/location_test.go @@ -277,6 +277,72 @@ func TestS3Location(t *testing.T) { } } +func TestOCILocation(t *testing.T) { + cases := []struct { + name string + conf OCIConfig + namespace string + host string + storagePath string + want string + }{ + { + name: "normal", + conf: OCIConfig{Bucket: "mybucket"}, + namespace: "myns", + host: "objectstorage.us-ashburn-1.oraclecloud.com", + storagePath: "foo.mp4", + want: "https://objectstorage.us-ashburn-1.oraclecloud.com/n/myns/b/mybucket/o/foo.mp4", + }, + { + name: "nested path", + conf: OCIConfig{Bucket: "mybucket"}, + namespace: "myns", + host: "objectstorage.us-ashburn-1.oraclecloud.com", + storagePath: "a/b/c.mp4", + want: "https://objectstorage.us-ashburn-1.oraclecloud.com/n/myns/b/mybucket/o/a/b/c.mp4", + }, + { + name: "storagePath with leading slash", + conf: OCIConfig{Bucket: "mybucket"}, + namespace: "myns", + host: "objectstorage.us-ashburn-1.oraclecloud.com", + storagePath: "/foo.mp4", + want: "https://objectstorage.us-ashburn-1.oraclecloud.com/n/myns/b/mybucket/o/foo.mp4", + }, + { + name: "space in key", + conf: OCIConfig{Bucket: "mybucket"}, + namespace: "myns", + host: "objectstorage.us-ashburn-1.oraclecloud.com", + storagePath: "folder/my file.mp4", + want: "https://objectstorage.us-ashburn-1.oraclecloud.com/n/myns/b/mybucket/o/folder/my%20file.mp4", + }, + { + name: "unicode in key", + conf: OCIConfig{Bucket: "mybucket"}, + namespace: "myns", + host: "objectstorage.us-ashburn-1.oraclecloud.com", + storagePath: "café/résumé.mp4", + want: "https://objectstorage.us-ashburn-1.oraclecloud.com/n/myns/b/mybucket/o/caf%C3%A9/r%C3%A9sum%C3%A9.mp4", + }, + { + name: "government realm host", + conf: OCIConfig{Bucket: "mybucket"}, + namespace: "myns", + host: "objectstorage.us-langley-1.oraclegovcloud.com", + storagePath: "foo.mp4", + want: "https://objectstorage.us-langley-1.oraclegovcloud.com/n/myns/b/mybucket/o/foo.mp4", + }, + } + for _, tc := range cases { + s := &ociStorage{conf: &tc.conf, namespace: tc.namespace, host: tc.host} + got := s.location(tc.storagePath) + require.Equal(t, tc.want, got, tc.name) + requireWellFormedHTTPLocation(t, got, tc.name) + } +} + func TestLocalLocation(t *testing.T) { cases := []struct { name string diff --git a/oci.go b/oci.go new file mode 100644 index 0000000..1b7f5b5 --- /dev/null +++ b/oci.go @@ -0,0 +1,302 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package storage + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "net/url" + "os" + "path" + "strings" + "time" + + "github.com/oracle/oci-go-sdk/v65/common" + "github.com/oracle/oci-go-sdk/v65/common/auth" + "github.com/oracle/oci-go-sdk/v65/objectstorage" +) + +type ociStorage struct { + conf *OCIConfig + client objectstorage.ObjectStorageClient + namespace string + host string +} + +func NewOCI(conf *OCIConfig) (Storage, error) { + provider, err := newConfigProvider(conf) + if err != nil { + return nil, err + } + + client, err := objectstorage.NewObjectStorageClientWithConfigurationProvider(provider) + if err != nil { + return nil, err + } + + if conf.Region != "" { + client.SetRegion(conf.Region) + } + if conf.Endpoint != "" { + client.Host = conf.Endpoint + } + + // The SDK only prepends a scheme to client.Host on the first request, so + // read it before GetNamespace below. + host := strings.TrimPrefix(client.Host, "https://") + host = strings.TrimPrefix(host, "http://") + + s := &ociStorage{ + conf: conf, + client: client, + namespace: conf.Namespace, + host: host, + } + + if s.namespace == "" { + req := objectstorage.GetNamespaceRequest{} + if conf.CompartmentID != "" { + req.CompartmentId = &conf.CompartmentID + } + resp, err := client.GetNamespace(context.Background(), req) + if err != nil { + return nil, wrapOCIError(err) + } + s.namespace = *resp.Value + } + + return s, nil +} + +func newConfigProvider(conf *OCIConfig) (common.ConfigurationProvider, error) { + if conf.UseWorkloadIdentity { + return auth.OkeWorkloadIdentityConfigurationProvider() + } + + if conf.TenancyOCID != "" && conf.UserOCID != "" && conf.PrivateKey != "" { + var passphrase *string + if conf.PrivateKeyPassphrase != "" { + passphrase = &conf.PrivateKeyPassphrase + } + return common.NewRawConfigurationProvider( + conf.TenancyOCID, + conf.UserOCID, + conf.Region, + conf.Fingerprint, + conf.PrivateKey, + passphrase, + ), nil + } + + return common.DefaultConfigProvider(), nil +} + +func (s *ociStorage) UploadData(data []byte, storagePath, contentType string) (string, int64, error) { + size := int64(len(data)) + body := common.NewOCIReadSeekCloser(io.NopCloser(bytes.NewReader(data))) + + location, err := s.upload(body, size, storagePath, contentType) + if err != nil { + return "", 0, err + } + return location, size, nil +} + +func (s *ociStorage) UploadFile(filepath, storagePath, contentType string) (string, int64, error) { + file, err := os.Open(filepath) + if err != nil { + return "", 0, err + } + defer func() { + _ = file.Close() + }() + + stat, err := file.Stat() + if err != nil { + return "", 0, err + } + + location, err := s.upload(file, stat.Size(), storagePath, contentType) + if err != nil { + return "", 0, err + } + return location, stat.Size(), nil +} + +// upload takes a seekable body: the SDK re-reads it when it retries. +func (s *ociStorage) upload(body io.ReadCloser, size int64, storagePath, contentType string) (string, error) { + contentDisposition := "inline" + _, err := s.client.PutObject(context.Background(), objectstorage.PutObjectRequest{ + NamespaceName: &s.namespace, + BucketName: &s.conf.Bucket, + ObjectName: &storagePath, + PutObjectBody: body, + ContentLength: &size, + ContentType: &contentType, + ContentDisposition: &contentDisposition, + }) + if err != nil { + return "", wrapOCIError(err) + } + + return s.location(storagePath), nil +} + +func (s *ociStorage) location(storagePath string) string { + return (&url.URL{ + Scheme: "https", + Host: s.host, + Path: path.Join("/n", s.namespace, "b", s.conf.Bucket, "o", storagePath), + }).String() +} + +func (s *ociStorage) ListObjects(prefix string) ([]string, error) { + var objects []string + var start *string + + for { + resp, err := s.client.ListObjects(context.Background(), objectstorage.ListObjectsRequest{ + NamespaceName: &s.namespace, + BucketName: &s.conf.Bucket, + Prefix: &prefix, + Start: start, + }) + if err != nil { + return nil, wrapOCIError(err) + } + + for _, obj := range resp.Objects { + objects = append(objects, *obj.Name) + } + + if resp.NextStartWith == nil { + return objects, nil + } + start = resp.NextStartWith + } +} + +func (s *ociStorage) DownloadData(storagePath string) ([]byte, error) { + resp, err := s.download(storagePath) + if err != nil { + return nil, err + } + defer func() { + _ = resp.Content.Close() + }() + + data, err := io.ReadAll(resp.Content) + if err != nil { + return nil, wrapOCIError(err) + } + return data, nil +} + +func (s *ociStorage) DownloadFile(filepath, storagePath string) (int64, error) { + file, err := os.Create(filepath) + if err != nil { + return 0, err + } + defer file.Close() + + resp, err := s.download(storagePath) + if err != nil { + return 0, err + } + defer func() { + _ = resp.Content.Close() + }() + + if _, err = io.Copy(file, resp.Content); err != nil { + return 0, wrapOCIError(err) + } + + return *resp.ContentLength, nil +} + +func (s *ociStorage) download(storagePath string) (objectstorage.GetObjectResponse, error) { + resp, err := s.client.GetObject(context.Background(), objectstorage.GetObjectRequest{ + NamespaceName: &s.namespace, + BucketName: &s.conf.Bucket, + ObjectName: &storagePath, + }) + return resp, wrapOCIError(err) +} + +// GeneratePresignedUrl creates a pre-authenticated request. Unlike an S3 +// presigned URL, a PAR is a durable object: it is listable and revocable, it +// costs an API call to mint, and it needs PAR_MANAGE on the bucket. Its access +// URI is returned only here and cannot be read back afterwards. +func (s *ociStorage) GeneratePresignedUrl(storagePath string, expiration time.Duration) (string, error) { + name := fmt.Sprintf("livekit-storage-%s-%d", storagePath, time.Now().UnixNano()) + expires := common.SDKTime{Time: time.Now().Add(expiration)} + + resp, err := s.client.CreatePreauthenticatedRequest(context.Background(), objectstorage.CreatePreauthenticatedRequestRequest{ + NamespaceName: &s.namespace, + BucketName: &s.conf.Bucket, + CreatePreauthenticatedRequestDetails: objectstorage.CreatePreauthenticatedRequestDetails{ + Name: &name, + AccessType: objectstorage.CreatePreauthenticatedRequestDetailsAccessTypeObjectread, + ObjectName: &storagePath, + TimeExpires: &expires, + }, + }) + if err != nil { + return "", wrapOCIError(err) + } + + return (&url.URL{ + Scheme: "https", + Host: s.host, + Path: *resp.AccessUri, + }).String(), nil +} + +func (s *ociStorage) DeleteObject(storagePath string) error { + _, err := s.client.DeleteObject(context.Background(), objectstorage.DeleteObjectRequest{ + NamespaceName: &s.namespace, + BucketName: &s.conf.Bucket, + ObjectName: &storagePath, + }) + return wrapOCIError(err) +} + +// DeleteObjects deletes one at a time — OCI has no bulk equivalent — and stops +// at the first failure, leaving earlier deletions in place, as gcp.go does. +func (s *ociStorage) DeleteObjects(storagePaths []string) error { + for _, storagePath := range storagePaths { + if err := s.DeleteObject(storagePath); err != nil { + return err + } + } + return nil +} + +func wrapOCIError(err error) error { + if err == nil { + return nil + } + var se common.ServiceError + if errors.As(err, &se) { + return &ErrorWithStatusCode{ + Err: err, + StatusCode: se.GetHTTPStatusCode(), + } + } + return err +} diff --git a/oci_test.go b/oci_test.go new file mode 100644 index 0000000..b08b427 --- /dev/null +++ b/oci_test.go @@ -0,0 +1,65 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package storage + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewConfigProvider(t *testing.T) { + // The workload identity branch is not covered: it reaches for a Kubernetes + // service account token and the proxymux endpoint, so off-cluster it can + // only fail, and asserting on that pins an SDK error string. + + t.Run("api key credentials", func(t *testing.T) { + conf := &OCIConfig{ + TenancyOCID: "ocid1.tenancy.oc1..tenancy", + UserOCID: "ocid1.user.oc1..user", + Fingerprint: "aa:bb:cc", + PrivateKey: "-----BEGIN PRIVATE KEY-----\nnot-a-real-key\n-----END PRIVATE KEY-----", + Region: "us-ashburn-1", + } + + provider, err := newConfigProvider(conf) + require.NoError(t, err) + require.NotNil(t, provider) + + tenancy, err := provider.TenancyOCID() + require.NoError(t, err) + require.Equal(t, conf.TenancyOCID, tenancy) + + user, err := provider.UserOCID() + require.NoError(t, err) + require.Equal(t, conf.UserOCID, user) + + fingerprint, err := provider.KeyFingerprint() + require.NoError(t, err) + require.Equal(t, conf.Fingerprint, fingerprint) + + region, err := provider.Region() + require.NoError(t, err) + require.Equal(t, conf.Region, region) + }) + + t.Run("falls back to the default provider", func(t *testing.T) { + // Only that a provider comes back — what it resolves to depends on the + // ambient OCI_CLI_* env and ~/.oci/config. + provider, err := newConfigProvider(&OCIConfig{Bucket: "mybucket"}) + require.NoError(t, err) + require.NotNil(t, provider) + }) +} diff --git a/storage_test.go b/storage_test.go index 8f538d7..0632202 100644 --- a/storage_test.go +++ b/storage_test.go @@ -119,6 +119,34 @@ func TestOCI(t *testing.T) { testStorage(t, s) } +func TestOCINative(t *testing.T) { + tenancy := os.Getenv("OCI_TENANCY_OCID") + user := os.Getenv("OCI_USER_OCID") + fingerprint := os.Getenv("OCI_FINGERPRINT") + privateKey := os.Getenv("OCI_PRIVATE_KEY") + namespace := os.Getenv("OCI_NAMESPACE") + region := os.Getenv("OCI_REGION") + bucket := os.Getenv("OCI_NATIVE_BUCKET") + + if tenancy == "" || user == "" || fingerprint == "" || privateKey == "" || + namespace == "" || region == "" || bucket == "" { + t.Skip("Missing env vars") + } + + s, err := storage.New(&storage.OCIConfig{ + TenancyOCID: tenancy, + UserOCID: user, + Fingerprint: fingerprint, + PrivateKey: privateKey, + Namespace: namespace, + Region: region, + Bucket: bucket, + }) + require.NoError(t, err) + + testStorage(t, s) +} + func TestSupabase(t *testing.T) { key := os.Getenv("SUPABASE_ACCESS_KEY") secret := os.Getenv("SUPABASE_SECRET") From 45de1aba69e1d6584510edcb979297dbec8753a7 Mon Sep 17 00:00:00 2001 From: David Colburn Date: Thu, 24 Sep 2026 10:52:06 -0400 Subject: [PATCH 2/2] Note the env vars OKE workload identity requires The OCI SDK reads OCI_RESOURCE_PRINCIPAL_VERSION and OCI_RESOURCE_PRINCIPAL_REGION from the environment when resolving a workload identity principal, and fails construction if either is unset. Nothing in the config surface hints at that, so setting the flag alone looks sufficient and isn't. --- config/config.go | 1 + 1 file changed, 1 insertion(+) diff --git a/config/config.go b/config/config.go index e710fd0..7e90a57 100644 --- a/config/config.go +++ b/config/config.go @@ -66,6 +66,7 @@ type OCIConfig struct { PrivateKey string `yaml:"private_key,omitempty"` // PEM contents, not a path PrivateKeyPassphrase string `yaml:"private_key_passphrase,omitempty"` + // Requires OCI_RESOURCE_PRINCIPAL_VERSION and OCI_RESOURCE_PRINCIPAL_REGION to be set in the environment. UseWorkloadIdentity bool `yaml:"use_workload_identity,omitempty"` Region string `yaml:"region,omitempty"`