Skip to content

Commit e9e7e76

Browse files
committed
Add Oracle TCPS Support
Signed-off-by: SajjadSadi074 <[email protected]>
1 parent b824c01 commit e9e7e76

File tree

88 files changed

+19134
-303
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+19134
-303
lines changed

go.mod

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@ require (
3434
go.mongodb.org/mongo-driver v1.14.0
3535
go.virtual-secrets.dev/apimachinery v0.0.1
3636
gomodules.xyz/pointer v0.1.0
37-
k8s.io/api v0.32.3
38-
k8s.io/apimachinery v0.32.3
39-
k8s.io/client-go v0.32.3
37+
k8s.io/api v0.32.8
38+
k8s.io/apimachinery v0.32.8
39+
k8s.io/client-go v0.32.8
4040
k8s.io/klog/v2 v2.130.1
41-
kmodules.xyz/client-go v0.32.7
41+
kmodules.xyz/client-go v0.32.9
4242
kmodules.xyz/custom-resources v0.32.2
43-
kubedb.dev/apimachinery v0.59.0
43+
kubedb.dev/apimachinery v0.59.1-0.20251204132717-657fbb84a6dd
4444
sigs.k8s.io/controller-runtime v0.20.4
4545
xorm.io/xorm v1.3.9
4646
)
@@ -161,17 +161,17 @@ require (
161161
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
162162
gopkg.in/inf.v0 v0.9.1 // indirect
163163
gopkg.in/yaml.v3 v3.0.1 // indirect
164-
k8s.io/apiextensions-apiserver v0.32.3 // indirect
164+
k8s.io/apiextensions-apiserver v0.32.8 // indirect
165165
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
166166
k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect
167167
kmodules.xyz/apiversion v0.2.0 // indirect
168-
kmodules.xyz/monitoring-agent-api v0.32.1 // indirect
168+
kmodules.xyz/monitoring-agent-api v0.32.4 // indirect
169169
kmodules.xyz/objectstore-api v0.32.0 // indirect
170170
kmodules.xyz/offshoot-api v0.32.0 // indirect
171171
kmodules.xyz/prober v0.32.0 // indirect
172172
kmodules.xyz/resource-metadata v0.32.1 // indirect
173173
kubeops.dev/operator-shard-manager v0.0.3 // indirect
174-
kubeops.dev/petset v0.0.12 // indirect
174+
kubeops.dev/petset v0.0.14 // indirect
175175
kubeops.dev/sidekick v0.0.11 // indirect
176176
kubestash.dev/apimachinery v0.21.0 // indirect
177177
modernc.org/memory v1.5.0 // indirect

go.sum

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -546,14 +546,14 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
546546
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
547547
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
548548
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
549-
k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls=
550-
k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
551-
k8s.io/apiextensions-apiserver v0.32.3 h1:4D8vy+9GWerlErCwVIbcQjsWunF9SUGNu7O7hiQTyPY=
552-
k8s.io/apiextensions-apiserver v0.32.3/go.mod h1:8YwcvVRMVzw0r1Stc7XfGAzB/SIVLunqApySV5V7Dss=
553-
k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U=
554-
k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
555-
k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU=
556-
k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY=
549+
k8s.io/api v0.32.8 h1:PhuKPnqsaXYuwmLXRLAmdDJ9EZ2R2kEbOZTq4UE3lGc=
550+
k8s.io/api v0.32.8/go.mod h1:gdRZQ4zXGawr9YrJ5OjTl7aR3TD0mTowtFsqFtpCDXo=
551+
k8s.io/apiextensions-apiserver v0.32.8 h1:iYIIaZmn/BMTwzGYRZnYZysaKB4t2TL3O+0yhmbXE2U=
552+
k8s.io/apiextensions-apiserver v0.32.8/go.mod h1:GTGskWgcBo/7boX33zcS8JY6vaG4s728AdbQPxtheVk=
553+
k8s.io/apimachinery v0.32.8 h1:95I+2jX71Tev+C+UlhNbmKfv+A/TQII42HLskiHZpBg=
554+
k8s.io/apimachinery v0.32.8/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
555+
k8s.io/client-go v0.32.8 h1:BkSFWUtRz/BbE3DJF98KPg7ix6lwMnIQ9DnHw3iWiSw=
556+
k8s.io/client-go v0.32.8/go.mod h1:vGkCzRxZ7BuRX2zdW7+kOwCdcgOkq9omDWb26wk/sE0=
557557
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
558558
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
559559
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
@@ -562,12 +562,12 @@ k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJ
562562
k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
563563
kmodules.xyz/apiversion v0.2.0 h1:vAQYqZFm4xu4pbB1cAdHbFEPES6EQkcR4wc06xdTOWk=
564564
kmodules.xyz/apiversion v0.2.0/go.mod h1:oPX8g8LvlPdPX3Yc5YvCzJHQnw3YF/X4/jdW0b1am80=
565-
kmodules.xyz/client-go v0.32.7 h1:vBAbp8vs4coYRhY4wqm1Hw/eBEDiVU238AyMLSoRJ1c=
566-
kmodules.xyz/client-go v0.32.7/go.mod h1:ZwLnc7UqEXUNSe43n/SnER6+7YAQCu38L2te6YefoHU=
565+
kmodules.xyz/client-go v0.32.9 h1:iZVhmTuMybHR7THGqnkbQdAJEOJCtZ9Ry9cY8TBvTJI=
566+
kmodules.xyz/client-go v0.32.9/go.mod h1:ZwLnc7UqEXUNSe43n/SnER6+7YAQCu38L2te6YefoHU=
567567
kmodules.xyz/custom-resources v0.32.2 h1:NkRqL/4AWHiXdT5WKFcJlBcvRuoNdeYIrBGvQIRJRn4=
568568
kmodules.xyz/custom-resources v0.32.2/go.mod h1:YKFNcsFQU7Z3AcPvYVCdFtgAdWiG1Wd1HQMOxCrAoWc=
569-
kmodules.xyz/monitoring-agent-api v0.32.1 h1:F0cm5NJWfgiANw3eiKkXXSXoClMBpAolMXE/N7Xts74=
570-
kmodules.xyz/monitoring-agent-api v0.32.1/go.mod h1:zgRKiJcuK7FOHy0Y1TsONRbJfgnPCs8t4Zh/6Afr+yU=
569+
kmodules.xyz/monitoring-agent-api v0.32.4 h1:JGm2bvHfAXHAf7EKjFrNDG3f7+QFpYV2Mvgj3RDVRhw=
570+
kmodules.xyz/monitoring-agent-api v0.32.4/go.mod h1:NkCiNP05EWrsjTTU2Npova/Sm27+I8vwUXqXVCmBbQ4=
571571
kmodules.xyz/objectstore-api v0.32.0 h1:A45lWKNb+02fJV1Mo4IDIpC1hWvLh/wuHKErovxKmQw=
572572
kmodules.xyz/objectstore-api v0.32.0/go.mod h1:N2SXdUU+YjXwG64UATYg+OoFYQ+p2MhX8B5TTKBeTf8=
573573
kmodules.xyz/offshoot-api v0.32.0 h1:gogc5scSZe2JoXtZof72UGRl3Tit0kFaFRMkLLT1D8o=
@@ -576,12 +576,12 @@ kmodules.xyz/prober v0.32.0 h1:8Z6pFRAu8kP0wwX2BooPCRy2SE6ZkUMHQmZDH5VUEGY=
576576
kmodules.xyz/prober v0.32.0/go.mod h1:h0fH4m9DaIwuNZq85zOlWUvBycyy4LvCPMUUhpS3iSE=
577577
kmodules.xyz/resource-metadata v0.32.1 h1:hWQbL0Xb+GaF7qn+rY0CNh7FUfKZw29VBUKTxjHFGYI=
578578
kmodules.xyz/resource-metadata v0.32.1/go.mod h1:wHC24BVzKb1gzkDCSI5l9CXK4AKD5gMamxEqVys50lI=
579-
kubedb.dev/apimachinery v0.59.0 h1:6daQ4dS6xayoyaZ67N5NXxOD1wH4H7v5JKPSwjPDbAk=
580-
kubedb.dev/apimachinery v0.59.0/go.mod h1:cdAy0z4ED/iunIQprmaB4yCSxgBkFaT5fcOT/ogxl0Q=
579+
kubedb.dev/apimachinery v0.59.1-0.20251204132717-657fbb84a6dd h1:AUYMIXpbpV3VqxKa63Wy4czifZy7VDWcUoQArZ3a11A=
580+
kubedb.dev/apimachinery v0.59.1-0.20251204132717-657fbb84a6dd/go.mod h1:8zu7zUBEd2PQsI0JZJFmxzglf63zxbwlAJIJlY77UqM=
581581
kubeops.dev/operator-shard-manager v0.0.3 h1:Z2YOAfyQIjvHMwT4O56lR0l9z25s2tCVDO22u/XuYnw=
582582
kubeops.dev/operator-shard-manager v0.0.3/go.mod h1:2oRq5vnCaUxzE+qIiRuzB34PlqahiynE+sYqWu6AMIY=
583-
kubeops.dev/petset v0.0.12 h1:NSFEeuckBVm44f3cAL4HhcQWvnfOE4qgbfug7+FEyaY=
584-
kubeops.dev/petset v0.0.12/go.mod h1:akG9QH1JaOZQcuQKEKWvkVWI8P3im/5O554aTRvB6Y0=
583+
kubeops.dev/petset v0.0.14 h1:Lk3prjtm5AgR44qr2SX8elx6sF9PK1G0GYlv8AZd9OY=
584+
kubeops.dev/petset v0.0.14/go.mod h1:X10jcvIjjP9HIa8ezh9PjtaXvFfk2zT+JmmO/S+7uhA=
585585
kubeops.dev/sidekick v0.0.11 h1:OydXdIH6cYSiWxKIWvrywk95WhhHSERkc7RNPOmTekc=
586586
kubeops.dev/sidekick v0.0.11/go.mod h1:90KMNmJOPoMKHbrdC1cpEsMx+1KjTea/lHDAbGRDzHc=
587587
kubestash.dev/apimachinery v0.21.0 h1:2qHROfY6RdxNjoEPm2yzQOuaqKlIeEMEn7bP+a/xezQ=

oracle/kubedb_client_builder.go

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@ import (
2020
"context"
2121
"database/sql"
2222
"fmt"
23+
"net/url"
24+
"os"
25+
"path/filepath"
2326

2427
olddbapi "kubedb.dev/apimachinery/apis/kubedb/v1alpha2"
2528
apiutils "kubedb.dev/apimachinery/pkg/utils"
2629

2730
"github.com/pkg/errors"
28-
_ "github.com/sijms/go-ora/v2" // Oracle driver
31+
go_ora "github.com/sijms/go-ora/v2"
2932
core "k8s.io/api/core/v1"
3033
"sigs.k8s.io/controller-runtime/pkg/client"
3134
)
@@ -37,6 +40,7 @@ type OracleClientBuilder struct {
3740
port int32
3841
service string
3942
ctx context.Context
43+
wallet string
4044
}
4145

4246
func NewOracleClientBuilder(kc client.Client, db *olddbapi.Oracle) *OracleClientBuilder {
@@ -66,6 +70,11 @@ func (o *OracleClientBuilder) WithContext(ctx context.Context) *OracleClientBuil
6670
return o
6771
}
6872

73+
func (o *OracleClientBuilder) WithWallet(wallet string) *OracleClientBuilder {
74+
o.wallet = wallet
75+
return o
76+
}
77+
6978
func (o *OracleClientBuilder) GetOracleClient() (*sql.DB, error) {
7079
if o.ctx == nil {
7180
o.ctx = context.Background()
@@ -76,6 +85,7 @@ func (o *OracleClientBuilder) GetOracleClient() (*sql.DB, error) {
7685
return nil, err
7786
}
7887

88+
// Fallback to standard connection (with wallet if configured)
7989
db, err := sql.Open("oracle", connStr)
8090
if err != nil {
8191
return nil, fmt.Errorf("failed to open Oracle connection: %v", err)
@@ -99,16 +109,69 @@ func (o *OracleClientBuilder) getConnectionString() (string, error) {
99109
return "", fmt.Errorf("failed to get auth credentials for Oracle %s/%s: %v", o.db.Namespace, o.db.Name, err)
100110
}
101111

102-
url := o.url
103-
if url == "" {
104-
url = PrimaryServiceDNS(o.db)
112+
serverURL := o.url
113+
if serverURL == "" {
114+
serverURL = PrimaryServiceDNS(o.db)
105115
}
106116
// Use the provided URL (e.g., service DNS)
107-
host := fmt.Sprintf("%v:%v/%v", url, o.port, o.service)
117+
host := fmt.Sprintf("%v:%v/%v", serverURL, o.port, o.service)
108118

109119
// Construct basic connection string
110-
connStr := fmt.Sprintf("oracle://%s:%s@%s", user, pass, host)
120+
connStr := ""
121+
122+
if o.db.Spec.TCPSConfig != nil && o.db.Spec.TCPSConfig.TLS != nil {
123+
// Constract connection string with wallet
124+
dbname := o.db.Name
125+
dstDir := o.wallet
126+
if dstDir == "" {
127+
dstDir = fmt.Sprintf("/tmp/%s/.tls-wallet", dbname)
128+
129+
if err := os.MkdirAll(dstDir, 0o755); err != nil {
130+
fmt.Printf("[ERROR] Failed to create wallet directory: %v\n", err)
131+
}
132+
133+
// Read the TLS secret from Kubernetes
134+
var tlsSecret core.Secret
135+
secretName := o.db.Name + "-tls-wallet"
136+
if err := o.kc.Get(o.ctx, client.ObjectKey{Namespace: o.db.Namespace, Name: secretName}, &tlsSecret); err != nil {
137+
return "", fmt.Errorf("failed to get TLS secret %s: %v", secretName, err)
138+
}
139+
140+
// Extract and save all files in the secret data
141+
for filename, data := range tlsSecret.Data {
142+
filePath := filepath.Join(dstDir, filename)
143+
if err := os.WriteFile(filePath, data, 0o600); err != nil {
144+
return "", fmt.Errorf("failed to write wallet file %s: %v", filename, err)
145+
}
146+
}
111147

148+
}
149+
150+
// Get service name from database spec
151+
service := "ORCL"
152+
if o.db.Spec.Listener != nil && o.db.Spec.Listener.Service != nil {
153+
service = *o.db.Spec.Listener.Service
154+
}
155+
156+
// Build connection string with SSL enabled
157+
baseURL := go_ora.BuildUrl(serverURL, int(o.port), service, user, pass, nil)
158+
159+
// Add SSL parameters with proper URL encoding
160+
params := url.Values{}
161+
params.Add("SSL", "true")
162+
params.Add("SSL VERIFY", "false")
163+
params.Add("WALLET", dstDir)
164+
params.Add("WALLET PASSWORD", pass)
165+
166+
// Build final connection string with parameters
167+
connStr = baseURL + "?" + params.Encode()
168+
for _, fname := range []string{"cwallet.sso", "ewallet.p12", "server.p12"} {
169+
filepath.Join(dstDir, fname)
170+
}
171+
} else {
172+
// Construct basic connection string without wallet
173+
connStr = fmt.Sprintf("oracle://%s:%s@%s", user, pass, host)
174+
}
112175
return connStr, nil
113176
}
114177

vendor/kmodules.xyz/client-go/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ ARCH := $(if $(GOARCH),$(GOARCH),$(shell go env GOARCH))
5858
BASEIMAGE_PROD ?= gcr.io/distroless/static-debian12
5959
BASEIMAGE_DBG ?= debian:12
6060

61-
GO_VERSION ?= 1.24
61+
GO_VERSION ?= 1.25
6262
BUILD_IMAGE ?= ghcr.io/appscode/golang-dev:$(GO_VERSION)
6363

6464
OUTBIN = bin/$(OS)_$(ARCH)/$(BIN)

vendor/kmodules.xyz/client-go/api/v1/cluster_enum.go

Lines changed: 3 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/kmodules.xyz/client-go/api/v1/object_enum.go

Lines changed: 3 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/kmodules.xyz/monitoring-agent-api/api/v1/appbinding.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ limitations under the License.
1616

1717
package v1
1818

19+
import (
20+
kmapi "kmodules.xyz/client-go/api/v1"
21+
)
22+
1923
type GrafanaConfig struct {
2024
URL string `json:"url"`
2125
Service ServiceSpec `json:"service"`
@@ -73,3 +77,46 @@ type GrafanaContext struct {
7377
FolderID *int64 `json:"folderID,omitempty"`
7478
Datasource string `json:"datasource,omitempty"`
7579
}
80+
81+
type Prometheus struct {
82+
AppBindingRef *kmapi.ObjectReference `json:"appBindingRef,omitempty"`
83+
*ConnectionSpec `json:",inline,omitempty"`
84+
}
85+
86+
// ConnectionSpec is the spec for app
87+
type ConnectionSpec struct {
88+
// ClientConfig defines how to communicate with the app.
89+
// Required
90+
ClientConfig `json:",inline"`
91+
92+
// Secret is the name of the secret to create in the AppBinding's
93+
// namespace that will hold the credentials associated with the AppBinding.
94+
AuthSecret *kmapi.ObjectReference `json:"authSecret,omitempty"`
95+
96+
// TLSSecret is the name of the secret that will hold
97+
// the client certificate and private key associated with the AppBinding.
98+
TLSSecret *kmapi.ObjectReference `json:"tlsSecret,omitempty"`
99+
}
100+
101+
// ClientConfig contains the information to make a connection with an app
102+
type ClientConfig struct {
103+
// `url` gives the location of the app, in standard URL form
104+
// (`[scheme://]host:port/path`). Exactly one of `url` or `service`
105+
// must be specified.
106+
// +optional
107+
URL string `json:"url"`
108+
109+
// InsecureSkipTLSVerify disables TLS certificate verification when communicating with this app.
110+
// This is strongly discouraged. You should use the CABundle instead.
111+
InsecureSkipTLSVerify bool `json:"insecureSkipTLSVerify,omitempty"`
112+
113+
// CABundle is a PEM encoded CA bundle which will be used to validate the serving certificate of this app.
114+
// +optional
115+
CABundle []byte `json:"caBundle,omitempty"`
116+
117+
// ServerName is used to verify the hostname on the returned
118+
// certificates unless InsecureSkipVerify is given. It is also included
119+
// in the client's handshake to support virtual hosting unless it is
120+
// an IP address.
121+
ServerName string `json:"serverName,omitempty"`
122+
}

vendor/kmodules.xyz/monitoring-agent-api/api/v1/helpers.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,17 @@ limitations under the License.
1717
package v1
1818

1919
import (
20+
"errors"
2021
"fmt"
2122

2223
"kmodules.xyz/client-go/policy/secomp"
24+
app_api "kmodules.xyz/custom-resources/apis/appcatalog/v1alpha1"
25+
appcatalog "kmodules.xyz/custom-resources/apis/appcatalog/v1alpha1"
2326

2427
"gomodules.xyz/pointer"
2528
core "k8s.io/api/core/v1"
29+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30+
"k8s.io/utils/ptr"
2631
)
2732

2833
func (agent *AgentSpec) SetDefaults() {
@@ -86,3 +91,50 @@ func GrafanaDatasource(isDefault bool, clusterName, projectId string) string {
8691
}
8792
return fmt.Sprintf("%s-%s", clusterName, projectId)
8893
}
94+
95+
func (c *ConnectionSpec) ToAppBinding() (*appcatalog.AppBinding, error) {
96+
var ns string
97+
if c.AuthSecret != nil {
98+
if c.AuthSecret.Namespace == "" {
99+
return nil, errors.New("auth secret namespace not set")
100+
}
101+
ns = c.AuthSecret.Namespace
102+
}
103+
if c.TLSSecret != nil {
104+
if c.TLSSecret.Namespace == "" {
105+
return nil, errors.New("tls secret namespace not set")
106+
}
107+
if ns != "" && ns != c.TLSSecret.Namespace {
108+
return nil, errors.New("tls secret namespace does not match auth secret namespace")
109+
}
110+
}
111+
112+
app := appcatalog.AppBinding{
113+
TypeMeta: metav1.TypeMeta{},
114+
ObjectMeta: metav1.ObjectMeta{
115+
Name: "<generated>",
116+
Namespace: ns,
117+
},
118+
Spec: appcatalog.AppBindingSpec{
119+
ClientConfig: appcatalog.ClientConfig{
120+
URL: ptr.To(c.URL),
121+
InsecureSkipTLSVerify: c.InsecureSkipTLSVerify,
122+
CABundle: c.CABundle,
123+
ServerName: c.ServerName,
124+
},
125+
},
126+
}
127+
if c.AuthSecret != nil {
128+
app.Spec.Secret = &app_api.TypedLocalObjectReference{
129+
Kind: "Secret", // It will create circular dependency, If we use Kubedb Constant .
130+
Name: c.AuthSecret.Name,
131+
}
132+
}
133+
if c.TLSSecret != nil {
134+
app.Spec.TLSSecret = &app_api.TypedLocalObjectReference{
135+
Kind: "Secret",
136+
Name: c.TLSSecret.Name,
137+
}
138+
}
139+
return &app, nil
140+
}

0 commit comments

Comments
 (0)