Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions cmd/builder/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -54,7 +55,6 @@ var (
systemBuildpacksDir string
extensionsDir string
downloadCacheDir string
err error
credhubConnectionAttempts int
credhubRetryDelay time.Duration
)
Expand Down Expand Up @@ -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{
Expand Down
14 changes: 14 additions & 0 deletions cmd/launcher/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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 {
Expand Down
48 changes: 48 additions & 0 deletions pkg/databaseuri/databaseuri.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package databaseuri

import (
"encoding/json"
"net/url"
)

func ParseDatabaseURI(services string) (string, error) {

@pbusko pbusko Oct 23, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tomkennedy513 is there an option to have a reusable version of this function? As you mentioned, this code must be the same across all existing lifecycles. It seems that the dockerapplifecycle imports some functions from the buildpackapplifecycle. Should this be extracted to a new repository which contains shared logic?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copied it over (though I did tweak it a bit) because I remember when I did the credhub interpolation there was a strong desire to not have any cross lifecycle dependencies. A third repo make senses but obviously requires a lot more coordination to get this change in

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we already have quite a lot of common things between the lifecycles, I believe it's time to introduce the library repository. What is your opinion on that?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think adding a library makes sense, but I think that should be separate from getting this fix in to unblock us.

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
}
110 changes: 110 additions & 0 deletions pkg/databaseuri/databaseuri_test.go
Original file line number Diff line number Diff line change
@@ -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"))
})
})