From 4bddce54e280d38ab723387d711f4c90e2436db3 Mon Sep 17 00:00:00 2001 From: Tom Kennedy Date: Fri, 12 Sep 2025 10:42:19 -0400 Subject: [PATCH] Add support for setting DATABASE_URL from vcap services - This is replicating behavior that exists in buildpack app lifecycle Signed-off-by: Tom Kennedy --- cmd/builder/cli/cli.go | 17 ++++- cmd/launcher/cli/cli.go | 14 ++++ pkg/databaseuri/databaseuri.go | 48 ++++++++++++ pkg/databaseuri/databaseuri_test.go | 110 ++++++++++++++++++++++++++++ 4 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 pkg/databaseuri/databaseuri.go create mode 100644 pkg/databaseuri/databaseuri_test.go diff --git a/cmd/builder/cli/cli.go b/cmd/builder/cli/cli.go index 7ec455b5..0eaa2dd9 100644 --- a/cmd/builder/cli/cli.go +++ b/cmd/builder/cli/cli.go @@ -13,6 +13,7 @@ import ( "code.cloudfoundry.org/cnbapplifecycle/pkg/archive" "code.cloudfoundry.org/cnbapplifecycle/pkg/buildpacks" "code.cloudfoundry.org/cnbapplifecycle/pkg/credhub" + "code.cloudfoundry.org/cnbapplifecycle/pkg/databaseuri" "code.cloudfoundry.org/cnbapplifecycle/pkg/errors" "code.cloudfoundry.org/cnbapplifecycle/pkg/keychain" "code.cloudfoundry.org/cnbapplifecycle/pkg/log" @@ -54,7 +55,6 @@ var ( systemBuildpacksDir string extensionsDir string downloadCacheDir string - err error credhubConnectionAttempts int credhubRetryDelay time.Duration ) @@ -95,7 +95,20 @@ var builderCmd = &cobra.Command{ if err := credhub.InterpolateServiceRefs(credhubConnectionAttempts, credhubRetryDelay); err != nil { logger.Error(err.Error()) - return errors.ErrLaunching + return errors.ErrGenericBuild + } + + databaseUrl, err := databaseuri.ParseDatabaseURI(os.Getenv("VCAP_SERVICES")) + if err != nil { + logger.Errorf("failed to parse database URI, error: %s\n", err.Error()) + return errors.ErrGenericBuild + } + if databaseUrl != "" { + err = os.Setenv("DATABASE_URL", databaseUrl) + if err != nil { + logger.Errorf("Unable to set DATABASE_URL envirionment variable: %v", err) + return errors.ErrGenericBuild + } } tempDirs := map[string]*string{ diff --git a/cmd/launcher/cli/cli.go b/cmd/launcher/cli/cli.go index cd78576b..7fb8596d 100644 --- a/cmd/launcher/cli/cli.go +++ b/cmd/launcher/cli/cli.go @@ -17,6 +17,7 @@ import ( builderCli "code.cloudfoundry.org/cnbapplifecycle/cmd/builder/cli" "code.cloudfoundry.org/cnbapplifecycle/pkg/credhub" + "code.cloudfoundry.org/cnbapplifecycle/pkg/databaseuri" "code.cloudfoundry.org/cnbapplifecycle/pkg/errors" "code.cloudfoundry.org/cnbapplifecycle/pkg/log" ) @@ -93,6 +94,19 @@ func Launch(osArgs []string, theLauncher TheLauncher) error { return errors.ErrLaunching } + databaseUrl, err := databaseuri.ParseDatabaseURI(os.Getenv("VCAP_SERVICES")) + if err != nil { + logger.Errorf("failed to parse database URI, error: %s\n", err.Error()) + return errors.ErrLaunching + } + if databaseUrl != "" { + err = os.Setenv("DATABASE_URL", databaseUrl) + if err != nil { + logger.Errorf("Unable to set DATABASE_URL envirionment variable: %v", err) + return errors.ErrLaunching + } + } + var self string var isSidecar bool if len(osArgs) > 1 { diff --git a/pkg/databaseuri/databaseuri.go b/pkg/databaseuri/databaseuri.go new file mode 100644 index 00000000..32dc2767 --- /dev/null +++ b/pkg/databaseuri/databaseuri.go @@ -0,0 +1,48 @@ +package databaseuri + +import ( + "encoding/json" + "net/url" +) + +func ParseDatabaseURI(services string) (string, error) { + if services == "" { + return "", nil + } + + data := map[string][]struct { + Credentials struct { + Uri string `json:"uri"` + } `json:"credentials"` + }{} + if err := json.Unmarshal([]byte(services), &data); err != nil { + return "", err + } + + var creds []string + for _, v1 := range data { + for _, v2 := range v1 { + if v2.Credentials.Uri != "" { + creds = append(creds, v2.Credentials.Uri) + } + } + } + + schemes := map[string]string{ + "mysql": "mysql2", + "mysql2": "", + "postgres": "", + "postgresql": "postgres", + } + for _, service_uri := range creds { + if uri, err := url.Parse(service_uri); err == nil { + if val, ok := schemes[uri.Scheme]; ok { + if val != "" { + uri.Scheme = val + } + return uri.String(), nil + } + } + } + return "", nil +} diff --git a/pkg/databaseuri/databaseuri_test.go b/pkg/databaseuri/databaseuri_test.go new file mode 100644 index 00000000..952eb45c --- /dev/null +++ b/pkg/databaseuri/databaseuri_test.go @@ -0,0 +1,110 @@ +package databaseuri_test + +import ( + "testing" + + "code.cloudfoundry.org/cnbapplifecycle/pkg/databaseuri" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestDatabaseuri(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Databaseuri Suite") +} + +var _ = Describe("ParseDatabaseURI", func() { + It("ignores services without credentials.uri", func() { + services := `{"eg":[{}]}` + uri, err := databaseuri.ParseDatabaseURI(services) + Expect(err).NotTo(HaveOccurred()) + Expect(uri).To(BeEmpty()) + }) + + It("returns empty when there are non relational database services", func() { + services := `{"eg":[{"credentials":{"uri":"sendgrid://foo:bar@host/db"}}]}` + uri, err := databaseuri.ParseDatabaseURI(services) + Expect(err).NotTo(HaveOccurred()) + Expect(uri).To(BeEmpty()) + }) + + It("returns empty when there are no services", func() { + services := "{}" + uri, err := databaseuri.ParseDatabaseURI(services) + Expect(err).NotTo(HaveOccurred()) + Expect(uri).To(BeEmpty()) + }) + + It("returns empty when services are empty", func() { + services := "" + uri, err := databaseuri.ParseDatabaseURI(services) + Expect(err).NotTo(HaveOccurred()) + Expect(uri).To(BeEmpty()) + }) + + Context("when there are relational database services", func() { + Context("with a mysql URI", func() { + It("changes the scheme to mysql2", func() { + services := `{"eg":[{"credentials":{"uri":"mysql://username:password@host/db"}}]}` + uri, err := databaseuri.ParseDatabaseURI(services) + Expect(err).NotTo(HaveOccurred()) + Expect(uri).To(Equal("mysql2://username:password@host/db")) + }) + }) + Context("with a mysql2 URI", func() { + It("returns the URI unchanged", func() { + services := `{"eg":[{"credentials":{"uri":"mysql2://username:password@host/db"}}]}` + uri, err := databaseuri.ParseDatabaseURI(services) + Expect(err).NotTo(HaveOccurred()) + Expect(uri).To(Equal("mysql2://username:password@host/db")) + }) + }) + Context("with a postgres URI", func() { + It("returns the URI unchanged", func() { + services := `{"eg":[{"credentials":{"uri":"postgres://username:password@host/db"}}]}` + uri, err := databaseuri.ParseDatabaseURI(services) + Expect(err).NotTo(HaveOccurred()) + Expect(uri).To(Equal("postgres://username:password@host/db")) + }) + }) + Context("with a postgresql URI", func() { + It("changes the scheme to postgres", func() { + services := `{"eg":[{"credentials":{"uri":"postgresql://username:password@host/db"}}]}` + uri, err := databaseuri.ParseDatabaseURI(services) + Expect(err).NotTo(HaveOccurred()) + Expect(uri).To(Equal("postgres://username:password@host/db")) + }) + }) + Context("with multiple relational database URIs", func() { + It("returns the first one found", func() { + services := `{ + "abc":[{"credentials":{"uri":"postgres://username:password@host/db1"}}, + {"credentials":{"uri":"postgres://username:password@host/db2"}}] + }` + uri, err := databaseuri.ParseDatabaseURI(services) + Expect(err).NotTo(HaveOccurred()) + Expect(uri).To(Equal("postgres://username:password@host/db1")) + }) + }) + Context("with an invalid URI", func() { + It("returns an empty string", func() { + services := `{"eg":[{"credentials":{"uri":"postgresql://invalid:password@host/%a"}}]}` + uri, err := databaseuri.ParseDatabaseURI(services) + Expect(err).NotTo(HaveOccurred()) + Expect(uri).To(Equal("")) + }) + }) + }) + + It("handles multiple services correctly", func() { + services := `{ + "abc":[{"credentials":{"uri":"u1"}}], + "def":[{"other":"data"}], + "ghi":[{"credentials":{"other":"data"}}], + "jkl":[{},{"credentials":{"uri":"mysql://username:password@host/db"}}] + }` + uri, err := databaseuri.ParseDatabaseURI(services) + Expect(err).NotTo(HaveOccurred()) + Expect(uri).To(Equal("mysql2://username:password@host/db")) + }) +})