diff --git a/README.md b/README.md index d47bfe3ad..6e9650f6a 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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. diff --git a/assets/catnip/Procfile b/assets/catnip/Procfile new file mode 100644 index 000000000..5efba6c76 --- /dev/null +++ b/assets/catnip/Procfile @@ -0,0 +1 @@ +web: catnip \ No newline at end of file diff --git a/assets/catnip/file/file.go b/assets/catnip/file/file.go new file mode 100644 index 000000000..f0aa0d60e --- /dev/null +++ b/assets/catnip/file/file.go @@ -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')) +} diff --git a/assets/catnip/router/router.go b/assets/catnip/router/router.go index 264f409db..b79cb6f94 100644 --- a/assets/catnip/router/router.go +++ b/assets/catnip/router/router.go @@ -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 { @@ -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 } diff --git a/cats_suite_helpers/cats_suite_helpers.go b/cats_suite_helpers/cats_suite_helpers.go index 38774814a..0d452d62f 100644 --- a/cats_suite_helpers/cats_suite_helpers.go +++ b/cats_suite_helpers/cats_suite_helpers.go @@ -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() { diff --git a/cats_suite_test.go b/cats_suite_test.go index 438f53803..3ffa32a48 100644 --- a/cats_suite_test.go +++ b/cats_suite_test.go @@ -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" diff --git a/file_based_service_bindings/file_based_service_bindings.go b/file_based_service_bindings/file_based_service_bindings.go new file mode 100644 index 000000000..114621cb1 --- /dev/null +++ b/file_based_service_bindings/file_based_service_bindings.go @@ -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") + }) +} diff --git a/helpers/assets/assets.go b/helpers/assets/assets.go index fff66521b..6c3eed0f4 100644 --- a/helpers/assets/assets.go +++ b/helpers/assets/assets.go @@ -3,6 +3,7 @@ package assets type Assets struct { AspClassic string Catnip string + CatnipSrc string CredHubEnabledApp string CredHubServiceBroker string Dora string @@ -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", diff --git a/helpers/config/config.go b/helpers/config/config.go index 36cef8f66..09edc5f66 100644 --- a/helpers/config/config.go +++ b/helpers/config/config.go @@ -13,6 +13,7 @@ type CatsConfig interface { GetIncludeDetect() bool GetIncludeDocker() bool GetIncludeCNB() bool + GetIncludeFileBasedServiceBindings() bool GetIncludeInternetDependent() bool GetIncludePrivateDockerRegistry() bool GetIncludeRouteServices() bool @@ -73,6 +74,7 @@ type CatsConfig interface { GetNamePrefix() string GetNginxBuildpackName() string GetNodejsBuildpackName() string + GetCNBGoBuildpackName() string GetCNBNodejsBuildpackName() string GetPrivateDockerRegistryImage() string GetPrivateDockerRegistryUsername() string diff --git a/helpers/config/config_struct.go b/helpers/config/config_struct.go index c9f169eb5..4bc05184d 100644 --- a/helpers/config/config_struct.go +++ b/helpers/config/config_struct.go @@ -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"` @@ -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"` @@ -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) @@ -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) @@ -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")) } @@ -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")) } @@ -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 } @@ -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 } diff --git a/helpers/config/config_test.go b/helpers/config/config_test.go index ee59d4f8b..106a5ec3e 100644 --- a/helpers/config/config_test.go +++ b/helpers/config/config_test.go @@ -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"` @@ -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"` @@ -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()) @@ -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")) @@ -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) @@ -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()) diff --git a/helpers/skip_messages/skip_messages.go b/helpers/skip_messages/skip_messages.go index 10ac0bf3d..28b1fad23 100644 --- a/helpers/skip_messages/skip_messages.go +++ b/helpers/skip_messages/skip_messages.go @@ -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'.