Skip to content

Commit

Permalink
Acceptance test for file based service bindings (#1374)
Browse files Browse the repository at this point in the history
Acceptance tests for file based service bindings

* pushes buildpack and CNB app, enables app feature "file-based-service-bindings" and binds a user-provided service
* then checks service binding files via app /file endpoint
  • Loading branch information
jochenehret authored Dec 17, 2024
1 parent 2848e56 commit 99633fa
Show file tree
Hide file tree
Showing 12 changed files with 199 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ include_app_syslog_tcp
* `include_deployments`: Flag to include tests for the cloud controller rolling deployments. V3 must also be enabled.
* `include_detect`: Flag to include tests in the detect group.
* `include_docker`: Flag to include tests related to running Docker apps on Diego. Diego must be deployed and the CC API diego_docker feature flag must be enabled for these tests to pass.
* `include_file_based_service_bindings`: Flag to include file-based service binding tests. For details, see [RFC0030](https://github.com/cloudfoundry/community/blob/main/toc/rfc/rfc-0030-add-support-for-file-based-service-binding.md)
* `include_http2_routing`: Flag to include the HTTP/2 Routing tests.
* `include_internet_dependent`: Flag to include tests that require the deployment to have internet access.
* `include_isolation_segments`: Flag to include isolation segment tests.
Expand Down Expand Up @@ -358,6 +359,7 @@ Test Group Name| Description
`cnb` | Tests our ability to use cloud native buildpacks.
`detect` | Tests the ability of the platform to detect the correct buildpack for compiling an application if no buildpack is explicitly specified.
`docker`| Tests our ability to run docker containers on Diego and that we handle docker metadata correctly.
`file-based service bindings`| Tests file-based service bindings for a buildpack and a CNB app.
`internet_dependent`| Tests the feature of being able to specify a buildpack via a Github URL. As such, this depends on your Cloud Foundry application containers having access to the Internet. You should take into account the configuration of the network into which you've deployed your Cloud Foundry, as well as any security group settings applied to application containers.
`isolation_segments` | This test group requires that Diego be deployed with a minimum of 2 cells. One of those cells must have been deployed with a `placement_tag`. If the deployment has been deployed with a routing isolation segment, `isolation_segment_domain` must also be set. For more information, please refer to the [Isolation Segments documentation](https://docs.cloudfoundry.org/adminguide/isolation-segments.html).
`route_services` | Tests the [Route Services](https://docs.cloudfoundry.org/services/route-services.html) feature of Cloud Foundry.
Expand Down
1 change: 1 addition & 0 deletions assets/catnip/Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: catnip
31 changes: 31 additions & 0 deletions assets/catnip/file/file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package file

import (
"fmt"
"github.com/go-chi/chi/v5"
"net/http"
"net/url"
"os"
)

func FileHandler(res http.ResponseWriter, req *http.Request) {
filename := chi.URLParam(req, "filename")
decodedFilename, err := url.PathUnescape(filename)
if err != nil {
http.Error(res, fmt.Sprintf("Cannot unescape file name: %s", filename), http.StatusBadRequest)
return
}

_, err = os.Stat(decodedFilename)
if err != nil {
http.Error(res, http.StatusText(404) + ": " + decodedFilename, 404)
return
}

content, err := os.ReadFile(decodedFilename)
if err != nil {
http.Error(res, http.StatusText(500) + ": " + err.Error(), 500)
return
}
res.Write(append(content, '\n'))
}
2 changes: 2 additions & 0 deletions assets/catnip/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/cloudfoundry/cf-acceptance-tests/assets/catnip/session"
"github.com/cloudfoundry/cf-acceptance-tests/assets/catnip/signal"
"github.com/cloudfoundry/cf-acceptance-tests/assets/catnip/text"
"github.com/cloudfoundry/cf-acceptance-tests/assets/catnip/file"
)

func New(out io.Writer, clock clock.Clock) *chi.Mux {
Expand All @@ -38,6 +39,7 @@ func New(out io.Writer, clock clock.Clock) *chi.Mux {
r.Get("/curl/{host}", linux.CurlHandler)
r.Get("/curl/{host}/", linux.CurlHandler)
r.Get("/curl/{host}/{port}", linux.CurlHandler)
r.Get("/file/{filename}", file.FileHandler)

return r
}
Expand Down
19 changes: 19 additions & 0 deletions cats_suite_helpers/cats_suite_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,25 @@ func CNBDescribe(description string, callback func()) bool {
})
}

const (
BuildpackLifecycle string = "buildpack"
CNBLifecycle = "CNB"
)

func FileBasedServiceBindingsDescribe(description string, lifecycle string, callback func()) bool {
return Describe(fmt.Sprintf("[file-based service bindings]", lifecycle), func() {
BeforeEach(func() {
if lifecycle == BuildpackLifecycle && !Config.GetIncludeFileBasedServiceBindings() {
Skip(skip_messages.SkipFileBasedServiceBindingsBuildpackApp)
}
if lifecycle == CNBLifecycle && (!Config.GetIncludeFileBasedServiceBindings() || !Config.GetIncludeCNB()) {
Skip(skip_messages.SkipFileBasedServiceBindingsCnbApp)
}
})
Describe(description, callback)
})
}

func InternetDependentDescribe(description string, callback func()) bool {
return Describe("[internet_dependent]", func() {
BeforeEach(func() {
Expand Down
1 change: 1 addition & 0 deletions cats_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
_ "github.com/cloudfoundry/cf-acceptance-tests/credhub"
_ "github.com/cloudfoundry/cf-acceptance-tests/detect"
_ "github.com/cloudfoundry/cf-acceptance-tests/docker"
_ "github.com/cloudfoundry/cf-acceptance-tests/file_based_service_bindings"
_ "github.com/cloudfoundry/cf-acceptance-tests/http2_routing"
_ "github.com/cloudfoundry/cf-acceptance-tests/internet_dependent"
_ "github.com/cloudfoundry/cf-acceptance-tests/isolation_segments"
Expand Down
115 changes: 115 additions & 0 deletions file_based_service_bindings/file_based_service_bindings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package file_based_service_bindings

import (
"encoding/json"
"fmt"
. "github.com/cloudfoundry/cf-acceptance-tests/cats_suite_helpers"
"github.com/cloudfoundry/cf-acceptance-tests/helpers/app_helpers"
"github.com/cloudfoundry/cf-acceptance-tests/helpers/assets"
"github.com/cloudfoundry/cf-acceptance-tests/helpers/random_name"
"github.com/cloudfoundry/cf-acceptance-tests/services"
"github.com/cloudfoundry/cf-test-helpers/v2/cf"
"github.com/cloudfoundry/cf-test-helpers/v2/generator"
"github.com/cloudfoundry/cf-test-helpers/v2/helpers"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/gexec"
"strings"
)

var _ = FileBasedServiceBindingsDescribe("Enabling file based service binding for a buildpack app", BuildpackLifecycle, func() {
callback(BuildpackLifecycle)
})

var _ = FileBasedServiceBindingsDescribe("Enabling file based service binding for a CNB app", CNBLifecycle, func() {
callback(CNBLifecycle)
})

var callback = func(lifecycle string) {
var appName, serviceName string

getEncodedFilepath := func(serviceName string, fileName string) string {
path := fmt.Sprintf("/etc/cf-service-bindings/%s/%s", serviceName, fileName)
return strings.Replace(path, "/", "%2F", -1)
}

checkFileContent := func(fileName string, content string) {
curlResponse := helpers.CurlApp(Config, appName, "/file/"+getEncodedFilepath(serviceName, fileName))
Expect(curlResponse).Should(ContainSubstring(content))
}

getServiceInstanceGuid := func(serviceName string) string {
serviceGuidCmd := cf.Cf("service", serviceName, "--guid")
Eventually(serviceGuidCmd).Should(Exit(0))
return strings.TrimSpace(string(serviceGuidCmd.Out.Contents()))
}

getServiceBindingGuid := func(appGuid string, instanceGuid string) string {
jsonResults := services_test.Response{}
bindingCurl := cf.Cf("curl", fmt.Sprintf("/v3/service_credential_bindings?app_guids=%s&service_instance_guids=%s", appGuid, instanceGuid)).Wait()
Expect(bindingCurl).To(Exit(0))
Expect(json.Unmarshal(bindingCurl.Out.Contents(), &jsonResults)).NotTo(HaveOccurred())
Expect(len(jsonResults.Resources)).To(BeNumerically(">", 0), "Expected to find at least one service binding.")
return jsonResults.Resources[0].GUID
}

BeforeEach(func() {
appName = random_name.CATSRandomName("APP")
serviceName = generator.PrefixedRandomName("cats", "svin") // uppercase characters are not valid
})

AfterEach(func() {
app_helpers.AppReport(appName)
Eventually(cf.Cf("unbind-service", appName, serviceName).Wait()).Should(Exit(0))
Eventually(cf.Cf("delete", appName, "-f")).Should(Exit(0))
Eventually(cf.Cf("delete-service", serviceName, "-f").Wait()).Should(Exit(0))
})

It("creates the required files in the app container", func() {
tags := "list, of, tags"
creds := `{"username": "admin", "password":"pa55woRD"}`
Expect(cf.Cf("create-user-provided-service", serviceName, "-p", creds, "-t", tags).Wait()).To(Exit(0))
serviceGuid := getServiceInstanceGuid(serviceName)

if lifecycle == BuildpackLifecycle {
Expect(cf.Cf("create-app", appName).Wait()).To(Exit(0))
}
if lifecycle == CNBLifecycle {
Expect(cf.Cf("create-app", appName, "--app-type", "cnb", "--buildpack", Config.GetGoBuildpackName()).Wait()).To(Exit(0))
}
appGuid := app_helpers.GetAppGuid(appName)

appFeatureUrl := fmt.Sprintf("/v3/apps/%s/features/file-based-service-bindings", appGuid)
Expect(cf.Cf("curl", appFeatureUrl, "-X", "PATCH", "-d", `{"enabled": true}`).Wait()).To(Exit(0))

Expect(cf.Cf("bind-service", appName, serviceName).Wait()).To(Exit(0))

if lifecycle == BuildpackLifecycle {
Expect(cf.Cf(app_helpers.CatnipWithArgs(
appName,
"-m", DEFAULT_MEMORY_LIMIT)...,
).Wait(Config.CfPushTimeoutDuration())).To(Exit(0))
}
if lifecycle == CNBLifecycle {
Expect(cf.Cf(
"push",
appName,
"--lifecycle", "cnb",
"--buildpack", Config.GetCNBGoBuildpackName(),
"-m", DEFAULT_MEMORY_LIMIT,
"-p", assets.NewAssets().CatnipSrc,
).Wait(Config.CfPushTimeoutDuration())).To(Exit(0))
}

checkFileContent("binding-guid", getServiceBindingGuid(appGuid, serviceGuid))
checkFileContent("instance-guid", serviceGuid)
checkFileContent("instance-name", serviceName)
checkFileContent("label", "user-provided")
checkFileContent("name", serviceName)
checkFileContent("password", "pa55woRD")
checkFileContent("provider", "user-provided")
checkFileContent("tags", `["list","of","tags"]`)
checkFileContent("type", "user-provided")
checkFileContent("username", "admin")
})
}
2 changes: 2 additions & 0 deletions helpers/assets/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package assets
type Assets struct {
AspClassic string
Catnip string
CatnipSrc string
CredHubEnabledApp string
CredHubServiceBroker string
Dora string
Expand Down Expand Up @@ -49,6 +50,7 @@ func NewAssets() Assets {
return Assets{
AspClassic: "assets/asp-classic",
Catnip: "assets/catnip/bin",
CatnipSrc: "assets/catnip",
CredHubEnabledApp: "assets/credhub-enabled-app/credhub-enabled-app.jar",
CredHubServiceBroker: "assets/credhub-service-broker",
Dora: "assets/dora",
Expand Down
2 changes: 2 additions & 0 deletions helpers/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type CatsConfig interface {
GetIncludeDetect() bool
GetIncludeDocker() bool
GetIncludeCNB() bool
GetIncludeFileBasedServiceBindings() bool
GetIncludeInternetDependent() bool
GetIncludePrivateDockerRegistry() bool
GetIncludeRouteServices() bool
Expand Down Expand Up @@ -73,6 +74,7 @@ type CatsConfig interface {
GetNamePrefix() string
GetNginxBuildpackName() string
GetNodejsBuildpackName() string
GetCNBGoBuildpackName() string
GetCNBNodejsBuildpackName() string
GetPrivateDockerRegistryImage() string
GetPrivateDockerRegistryUsername() string
Expand Down
16 changes: 16 additions & 0 deletions helpers/config/config_struct.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ type config struct {
RubyBuildpackName *string `json:"ruby_buildpack_name"`
StaticFileBuildpackName *string `json:"staticfile_buildpack_name"`

CNBGoBuildpackName *string `json:"cnb_go_buildpack_name"`
CNBNodejsBuildpackName *string `json:"cnb_nodejs_buildpack_name"`

VolumeServiceName *string `json:"volume_service_name"`
Expand All @@ -78,6 +79,7 @@ type config struct {
IncludeDetect *bool `json:"include_detect"`
IncludeDocker *bool `json:"include_docker"`
IncludeCNB *bool `json:"include_cnb"`
IncludeFileBasedServiceBindings *bool `json:"include_file_based_service_bindings"`
IncludeInternetDependent *bool `json:"include_internet_dependent"`
IncludeIsolationSegments *bool `json:"include_isolation_segments"`
IncludePrivateDockerRegistry *bool `json:"include_private_docker_registry"`
Expand Down Expand Up @@ -164,6 +166,7 @@ func getDefaults() config {
defaults.RubyBuildpackName = ptrToString("ruby_buildpack")
defaults.StaticFileBuildpackName = ptrToString("staticfile_buildpack")

defaults.CNBGoBuildpackName = ptrToString("docker://gcr.io/paketo-buildpacks/go:latest")
defaults.CNBNodejsBuildpackName = ptrToString("docker://gcr.io/paketo-buildpacks/nodejs:latest")

defaults.IncludeAppSyslogTCP = ptrToBool(true)
Expand All @@ -180,6 +183,7 @@ func getDefaults() config {
defaults.CredhubClientSecret = ptrToString("")
defaults.IncludeDocker = ptrToBool(false)
defaults.IncludeCNB = ptrToBool(false)
defaults.IncludeFileBasedServiceBindings = ptrToBool(false)
defaults.IncludeInternetDependent = ptrToBool(false)
defaults.IncludeIsolationSegments = ptrToBool(false)
defaults.IncludeTCPIsolationSegments = ptrToBool(false)
Expand Down Expand Up @@ -412,6 +416,9 @@ func validateConfig(config *config) error {
if config.StaticFileBuildpackName == nil {
errs = errors.Join(errs, fmt.Errorf("* 'staticfile_buildpack_name' must not be null"))
}
if config.CNBGoBuildpackName == nil {
errs = errors.Join(errs, fmt.Errorf("* 'cnb_go_buildpack_name' must not be null"))
}
if config.CNBNodejsBuildpackName == nil {
errs = errors.Join(errs, fmt.Errorf("* 'cnb_nodejs_buildpack_name' must not be null"))
}
Expand All @@ -430,6 +437,9 @@ func validateConfig(config *config) error {
if config.IncludeDocker == nil {
errs = errors.Join(errs, fmt.Errorf("* 'include_docker' must not be null"))
}
if config.IncludeFileBasedServiceBindings == nil {
errs = errors.Join(errs, fmt.Errorf("* 'include_file_based_service_bindings' must not be null"))
}
if config.IncludeCNB == nil {
errs = errors.Join(errs, fmt.Errorf("* 'include_cnb' must not be null"))
}
Expand Down Expand Up @@ -935,6 +945,8 @@ func (c *config) GetIncludeCNB() bool {
return *c.IncludeCNB
}

func (c *config) GetIncludeFileBasedServiceBindings() bool { return *c.IncludeFileBasedServiceBindings }

func (c *config) GetIncludeInternetDependent() bool {
return *c.IncludeInternetDependent
}
Expand Down Expand Up @@ -1087,6 +1099,10 @@ func (c *config) GetStaticFileBuildpackName() string {
return *c.StaticFileBuildpackName
}

func (c *config) GetCNBGoBuildpackName() string {
return *c.CNBGoBuildpackName
}

func (c *config) GetCNBNodejsBuildpackName() string {
return *c.CNBNodejsBuildpackName
}
Expand Down
6 changes: 6 additions & 0 deletions helpers/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ type testConfig struct {
IncludeDeployments *bool `json:"include_deployments,omitempty"`
IncludeDetect *bool `json:"include_detect,omitempty"`
IncludeDocker *bool `json:"include_docker,omitempty"`
IncludeFileBasedServiceBindings *bool `json:"include_file_based_service_bindings,omitempty"`
IncludeInternetDependent *bool `json:"include_internet_dependent,omitempty"`
IncludeIsolationSegments *bool `json:"include_isolation_segments,omitempty"`
IncludePrivateDockerRegistry *bool `json:"include_private_docker_registry,omitempty"`
Expand Down Expand Up @@ -149,6 +150,7 @@ type nullConfig struct {
IncludeContainerNetworking *bool `json:"include_container_networking"`
IncludeDetect *bool `json:"include_detect"`
IncludeDocker *bool `json:"include_docker"`
IncludeFileBasedServiceBindings *bool `json:"include_file_based_service_bindings"`
IncludeInternetDependent *bool `json:"include_internet_dependent"`
IncludePrivateDockerRegistry *bool `json:"include_private_docker_registry"`
IncludeRouteServices *bool `json:"include_route_services"`
Expand Down Expand Up @@ -275,6 +277,7 @@ var _ = Describe("Config", func() {
Expect(config.GetIncludeV3()).To(BeTrue())

Expect(config.GetIncludeDocker()).To(BeFalse())
Expect(config.GetIncludeFileBasedServiceBindings()).To(BeFalse())
Expect(config.GetIncludeInternetDependent()).To(BeFalse())
Expect(config.GetIncludeRouteServices()).To(BeFalse())
Expect(config.GetIncludeContainerNetworking()).To(BeFalse())
Expand Down Expand Up @@ -404,6 +407,7 @@ var _ = Describe("Config", func() {
Expect(err.Error()).To(ContainSubstring("'include_apps' must not be null"))
Expect(err.Error()).To(ContainSubstring("'include_detect' must not be null"))
Expect(err.Error()).To(ContainSubstring("'include_docker' must not be null"))
Expect(err.Error()).To(ContainSubstring("'include_file_based_service_bindings' must not be null"))
Expect(err.Error()).To(ContainSubstring("'include_internet_dependent' must not be null"))
Expect(err.Error()).To(ContainSubstring("'include_private_docker_registry' must not be null"))
Expect(err.Error()).To(ContainSubstring("'include_route_services' must not be null"))
Expand Down Expand Up @@ -463,6 +467,7 @@ var _ = Describe("Config", func() {
testCfg.IncludeDeployments = ptrToBool(true)
testCfg.IncludeDetect = ptrToBool(false)
testCfg.IncludeDocker = ptrToBool(true)
testCfg.IncludeFileBasedServiceBindings = ptrToBool(true)
testCfg.IncludeInternetDependent = ptrToBool(true)
testCfg.IncludeIsolationSegments = ptrToBool(true)
testCfg.IncludePrivateDockerRegistry = ptrToBool(true)
Expand Down Expand Up @@ -525,6 +530,7 @@ var _ = Describe("Config", func() {
Expect(config.GetIncludeDeployments()).To(BeTrue())
Expect(config.GetIncludeDetect()).To(BeFalse())
Expect(config.GetIncludeDocker()).To(BeTrue())
Expect(config.GetIncludeFileBasedServiceBindings()).To(BeTrue())
Expect(config.GetIncludeInternetDependent()).To(BeTrue())
Expect(config.GetIncludeIsolationSegments()).To(BeTrue())
Expect(config.GetIncludePrivateDockerRegistry()).To(BeTrue())
Expand Down
2 changes: 2 additions & 0 deletions helpers/skip_messages/skip_messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const SkipDockerMessage = `Skipping this test because config.IncludeDocker is se
NOTE: Ensure Docker containers are enabled on your platform before enabling this test.`
const SkipCNBMessage = `Skipping this test because config.IncludeCNB is set to 'false'.
NOTE: Ensure CNB lifecycle is enabled on your platform before enabling this test.`
const SkipFileBasedServiceBindingsBuildpackApp = `Skipping this test because config.IncludeFileBasedServiceBindings is set to 'false'.`
const SkipFileBasedServiceBindingsCnbApp = `Skipping this test because config.IncludeFileBasedServiceBindings and/or config.IncludeCNB are set to 'false'.`
const SkipInternetDependentMessage = `Skipping this test because config.IncludeInternetDependent is set to 'false'.
NOTE: Ensure that your platform has access to the internet before running this test.`
const SkipPrivateDockerRegistryMessage = `Skipping this test because config.IncludePrivateDockerRegistry is set to 'false'.
Expand Down

0 comments on commit 99633fa

Please sign in to comment.