diff --git a/.envrc b/.envrc index d0c0e47f0..db575ee60 100644 --- a/.envrc +++ b/.envrc @@ -10,4 +10,5 @@ export GSB_SERVICE_CSB_AWS_AURORA_MYSQL_PLANS='[{"name":"default","id":"10b2bd92 export GSB_SERVICE_CSB_AWS_MYSQL_PLANS='[{"name":"default","id":"0f3522b2-f040-443b-bc53-4aed25284840","description":"Default MySQL plan","display_name":"default","instance_class":"db.t3.micro","mysql_version":"8.0","storage_gb":100}]' export GSB_SERVICE_CSB_AWS_REDIS_PLANS='[{"name":"default", "id":"c7f64994-a1d9-4e1f-9491-9d8e56bbf146","description":"Default Redis plan","display_name":"default","node_type":"cache.t3.medium","redis_version": "6.0"},{"name" : "example-with-flexible-node-type","id" : "2deb6c13-7ea1-4bad-a519-0ac9600e9a29","description" : "An example of a Redis plan for which node_type can be specified at provision time. Replace with your own plan configuration.","redis_version" : "6.x","node_count" : 2}]' export GSB_SERVICE_CSB_AWS_MSSQL_PLANS='[{"name":"default","id":"7400cd8f-5f98-4457-8de0-03232ec12f62","description":"Default MSSQL plan","display_name":"default","engine":"sqlserver-se","mssql_version":"15.00","storage_gb":100, "instance_class":"db.r5.large" }]' +export GSB_SERVICE_CSB_AWS_SQS_PLANS='[{"name":"standard","id":"c2fdfc84-bf86-11ee-a4f5-8b0d531ce7e2","description":"Default SQS standard queue plan","display_name":"standard"}]' export GSB_BROKERPAK_CONFIG='{"global_labels":[{"key": "key1", "value": "value1"},{"key": "key2", "value": "value2"}]}' diff --git a/Makefile b/Makefile index 704da3759..9789051dc 100644 --- a/Makefile +++ b/Makefile @@ -25,13 +25,13 @@ ifeq ($(GO_OK), 0) # use local go binary GO=go GOFMT=gofmt BROKER_GO_OPTS=PORT=8080 \ - DB_TYPE=sqlite3 \ - DB_PATH=/tmp/csb-db \ - BROKERPAK_UPDATES_ENABLED=$(BROKERPAK_UPDATES_ENABLED) \ SECURITY_USER_NAME=$(SECURITY_USER_NAME) \ SECURITY_USER_PASSWORD=$(SECURITY_USER_PASSWORD) \ AWS_ACCESS_KEY_ID='$(AWS_ACCESS_KEY_ID)' \ AWS_SECRET_ACCESS_KEY=$(AWS_SECRET_ACCESS_KEY) \ + DB_TYPE=sqlite3 \ + DB_PATH=/tmp/csb-db \ + BROKERPAK_UPDATES_ENABLED=$(BROKERPAK_UPDATES_ENABLED) \ PAK_BUILD_CACHE_PATH=$(PAK_BUILD_CACHE_PATH) \ GSB_PROVISION_DEFAULTS='$(GSB_PROVISION_DEFAULTS)' \ GSB_SERVICE_CSB_AWS_S3_BUCKET_PLANS='$(GSB_SERVICE_CSB_AWS_S3_BUCKET_PLANS)' \ @@ -40,6 +40,7 @@ BROKER_GO_OPTS=PORT=8080 \ GSB_SERVICE_CSB_AWS_AURORA_MYSQL_PLANS='$(GSB_SERVICE_CSB_AWS_AURORA_MYSQL_PLANS)' \ GSB_SERVICE_CSB_AWS_MYSQL_PLANS='$(GSB_SERVICE_CSB_AWS_MYSQL_PLANS)' \ GSB_SERVICE_CSB_AWS_REDIS_PLANS='$(GSB_SERVICE_CSB_AWS_REDIS_PLANS)' \ + GSB_SERVICE_CSB_AWS_SQS_PLANS='$(GSB_SERVICE_CSB_AWS_SQS_PLANS)' \ GSB_COMPATIBILITY_ENABLE_BETA_SERVICES='$(GSB_COMPATIBILITY_ENABLE_BETA_SERVICES)' PAK_PATH=$(PWD) @@ -55,6 +56,7 @@ BROKER_DOCKER_OPTS=--rm -v $(PAK_BUILD_CACHE_PATH):$(PAK_BUILD_CACHE_PATH) -v $( -e AWS_SECRET_ACCESS_KEY \ -e "DB_TYPE=sqlite3" \ -e "DB_PATH=/tmp/csb-db" \ + -e BROKERPAK_UPDATES_ENABLED \ -e PAK_BUILD_CACHE_PATH=$(PAK_BUILD_CACHE_PATH) \ -e GSB_PROVISION_DEFAULTS \ -e GSB_SERVICE_CSB_AWS_S3_BUCKET_PLANS \ @@ -63,6 +65,7 @@ BROKER_DOCKER_OPTS=--rm -v $(PAK_BUILD_CACHE_PATH):$(PAK_BUILD_CACHE_PATH) -v $( -e GSB_SERVICE_CSB_AWS_AURORA_MYSQL_PLANS \ -e GSB_SERVICE_CSB_AWS_MYSQL_PLANS \ -e GSB_SERVICE_CSB_AWS_REDIS_PLANS \ + -e GSB_SERVICE_CSB_AWS_SQS_PLANS \ -e GSB_COMPATIBILITY_ENABLE_BETA_SERVICES RUN_CSB=docker run $(BROKER_DOCKER_OPTS) $(CSB_DOCKER_IMAGE) diff --git a/aws-sqs.yml b/aws-sqs.yml new file mode 100644 index 000000000..beb2793db --- /dev/null +++ b/aws-sqs.yml @@ -0,0 +1,59 @@ +version: 1 +name: csb-aws-sqs +id: 2198d694-bf85-11ee-a918-a7bdfa69a96d +description: CSB AWS SQS +display_name: CSB AWS SQS +image_url: file://service-images/csb.png +documentation_url: https://docs.vmware.com/en/Cloud-Service-Broker-for-VMware-Tanzu/index.html +provider_display_name: VMware +support_url: https://aws.amazon.com/sqs/ +tags: [aws, sqs, beta] +plan_updateable: true +provision: + user_inputs: + - field_name: region + type: string + details: The region of AWS. + default: us-west-2 + constraints: + examples: + - us-west-2 + - eu-west-1 + pattern: ^[a-z][a-z0-9-]+$ + prohibit_update: true + - field_name: aws_access_key_id + type: string + details: AWS access key + default: ${config("aws.access_key_id")} + - field_name: aws_secret_access_key + type: string + details: AWS secret key + default: ${config("aws.secret_access_key")} + computed_inputs: + - name: instance_name + default: csb-sqs-${request.instance_id} + overwrite: true + type: string + - name: labels + default: ${json.marshal(request.default_labels)} + overwrite: true + type: object + template_refs: + main: terraform/sqs/provision/main.tf + outputs: terraform/sqs/provision/outputs.tf + provider: terraform/sqs/provision/providers.tf + versions: terraform/sqs/provision/versions.tf + variables: terraform/sqs/provision/variables.tf + outputs: + - field_name: arn + type: string + details: ARN for the queue + - field_name: url + type: string + details: URL for the queue + - field_name: name + type: string + details: name for the queue + - field_name: region + type: string + details: AWS region for the queue \ No newline at end of file diff --git a/integration-tests/integration_test_suite_test.go b/integration-tests/integration_test_suite_test.go index d4c23fe51..08fde07e4 100644 --- a/integration-tests/integration_test_suite_test.go +++ b/integration-tests/integration_test_suite_test.go @@ -48,6 +48,7 @@ var _ = BeforeSuite(func() { "GSB_SERVICE_CSB_AWS_MYSQL_PLANS=" + marshall(customMySQLPlans), "GSB_SERVICE_CSB_AWS_REDIS_PLANS=" + marshall(customRedisPlans), "GSB_SERVICE_CSB_AWS_MSSQL_PLANS=" + marshall(customMSSQLPlans), + "GSB_SERVICE_CSB_AWS_SQS_PLANS=" + marshall(customSQSPlans), "AWS_ACCESS_KEY_ID=" + awsAccessKeyID, "AWS_SECRET_ACCESS_KEY=" + awsSecretAccessKey, "CSB_LISTENER_HOST=localhost", diff --git a/integration-tests/sqs_test.go b/integration-tests/sqs_test.go new file mode 100644 index 000000000..d7ff6ba2f --- /dev/null +++ b/integration-tests/sqs_test.go @@ -0,0 +1,156 @@ +package integration_test + +import ( + "fmt" + + testframework "github.com/cloudfoundry/cloud-service-broker/brokerpaktestframework" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gstruct" +) + +const ( + sqsServiceID = "2198d694-bf85-11ee-a918-a7bdfa69a96d" + sqsServiceName = "csb-aws-sqs" + sqsServiceDescription = "CSB AWS SQS" + sqsServiceDisplayName = "CSB AWS SQS" + sqsServiceSupportURL = "https://aws.amazon.com/sqs/" + sqsServiceProviderDisplayName = "VMware" + sqsCustomStandardPlanName = "custom-standard" + sqsCustomStandardPlanID = "4c206ad6-bf89-11ee-8900-2f8e8940fc0d" +) + +var customSQSPlans = []map[string]any{ + customSQSPlan, +} + +var customSQSPlan = map[string]any{ + "name": sqsCustomStandardPlanName, + "id": sqsCustomStandardPlanID, + "description": "Custom SQS standard queue plan", + "metadata": map[string]any{ + "displayName": "custom-standard", + }, +} + +var _ = Describe("SQS", Label("SQS"), func() { + BeforeEach(func() { + Expect(mockTerraform.SetTFState([]testframework.TFStateValue{})).To(Succeed()) + + DeferCleanup(func() { + Expect(mockTerraform.Reset()).To(Succeed()) + }) + }) + + It("should publish AWS SQS in the catalog", func() { + catalog, err := broker.Catalog() + Expect(err).NotTo(HaveOccurred()) + + service := testframework.FindService(catalog, sqsServiceName) + Expect(service.ID).To(Equal(sqsServiceID)) + Expect(service.Description).To(Equal(sqsServiceDescription)) + Expect(service.Tags).To(ConsistOf("aws", "sqs", "beta")) + Expect(service.Metadata.DisplayName).To(Equal(sqsServiceDisplayName)) + Expect(service.Metadata.DocumentationUrl).To(Equal(documentationURL)) + Expect(service.Metadata.ImageUrl).To(ContainSubstring("data:image/png;base64,")) + Expect(service.Metadata.SupportUrl).To(Equal(sqsServiceSupportURL)) + Expect(service.Metadata.ProviderDisplayName).To(Equal(sqsServiceProviderDisplayName)) + Expect(service.Plans).To( + ConsistOf( + MatchFields(IgnoreExtras, Fields{ + Name: Equal(sqsCustomStandardPlanName), + ID: Equal(sqsCustomStandardPlanID), + }), + ), + ) + }) + + Describe("provisioning", func() { + DescribeTable("property constraints", + func(params map[string]any, expectedErrorMsg string) { + _, err := broker.Provision(sqsServiceName, sqsCustomStandardPlanName, params) + + Expect(err).To(MatchError(ContainSubstring(expectedErrorMsg))) + }, + Entry( + "invalid region", + map[string]any{"region": "-Asia-northeast1"}, + "region: Does not match pattern '^[a-z][a-z0-9-]+$'", + ), + ) + + It("should provision a plan", func() { + instanceID, err := broker.Provision(sqsServiceName, sqsCustomStandardPlanName, nil) + Expect(err).NotTo(HaveOccurred()) + + Expect(mockTerraform.FirstTerraformInvocationVars()).To( + SatisfyAll( + HaveKeyWithValue("labels", MatchKeys(IgnoreExtras, Keys{ + "pcf-instance-id": Equal(instanceID), + "key1": Equal("value1"), + "key2": Equal("value2"), + })), + HaveKeyWithValue("instance_name", fmt.Sprintf("csb-sqs-%s", instanceID)), + HaveKeyWithValue("region", fakeRegion), + HaveKeyWithValue("aws_access_key_id", awsAccessKeyID), + HaveKeyWithValue("aws_secret_access_key", awsSecretAccessKey), + ), + ) + }) + + It("should allow properties to be set on provision", func() { + _, err := broker.Provision(sqsServiceName, sqsCustomStandardPlanName, map[string]any{ + "region": "africa-north-4", + "aws_access_key_id": "fake-aws-access-key-id", + "aws_secret_access_key": "fake-aws-secret-access-key", + }) + Expect(err).NotTo(HaveOccurred()) + + Expect(mockTerraform.FirstTerraformInvocationVars()).To( + SatisfyAll( + HaveKeyWithValue("region", "africa-north-4"), + HaveKeyWithValue("aws_access_key_id", "fake-aws-access-key-id"), + HaveKeyWithValue("aws_secret_access_key", "fake-aws-secret-access-key"), + ), + ) + }) + }) + + Describe("updating instance", func() { + var instanceID string + + BeforeEach(func() { + var err error + instanceID, err = broker.Provision(sqsServiceName, sqsCustomStandardPlanName, nil) + + Expect(err).NotTo(HaveOccurred()) + }) + + DescribeTable("should prevent updating properties flagged as `prohibit_update` because it can result in the recreation of the service instance", + func(prop string, value any) { + err := broker.Update(instanceID, sqsServiceName, sqsCustomStandardPlanName, map[string]any{prop: value}) + + Expect(err).To(MatchError( + ContainSubstring( + "attempt to update parameter that may result in service instance re-creation and data loss", + ), + )) + + const initialProvisionInvocation = 1 + Expect(mockTerraform.ApplyInvocations()).To(HaveLen(initialProvisionInvocation)) + }, + Entry("update region", "region", "no-matter-what-region"), + ) + + DescribeTable( + "some allowed updates", + func(prop string, value any) { + err := broker.Update(instanceID, sqsServiceName, sqsCustomStandardPlanName, map[string]any{prop: value}) + + Expect(err).NotTo(HaveOccurred()) + }, + Entry(nil, "aws_access_key_id", "fake-aws-access-key-id"), + Entry(nil, "aws_secret_access_key", "fake-aws-secret-access-key"), + ) + }) +}) diff --git a/manifest.yml b/manifest.yml index 67191b8d1..90afcb3d1 100644 --- a/manifest.yml +++ b/manifest.yml @@ -57,6 +57,7 @@ service_definitions: - aws-aurora-postgresql.yml - aws-aurora-mysql.yml - aws-mssql.yml +- aws-sqs.yml diff --git a/scripts/push-broker.sh b/scripts/push-broker.sh index 9cb744061..81598f37b 100755 --- a/scripts/push-broker.sh +++ b/scripts/push-broker.sh @@ -119,6 +119,12 @@ if [[ -z "$GSB_SERVICE_CSB_AWS_MSSQL_PLANS" ]]; then fi echo " GSB_SERVICE_CSB_AWS_MSSQL_PLANS: $(echo "$GSB_SERVICE_CSB_AWS_MSSQL_PLANS" | jq @json)" >>$cfmf +if [[ -z "$GSB_SERVICE_CSB_AWS_SQS_PLANS" ]]; then + echo "Missing GSB_SERVICE_CSB_AWS_SQS_PLANS variable" + exit 1 +fi +echo " GSB_SERVICE_CSB_AWS_SQS_PLANS: $(echo "$GSB_SERVICE_CSB_AWS_SQS_PLANS" | jq @json)" >>$cfmf + cf push --no-start -f "${cfmf}" --var app=${APP_NAME} if [[ -z ${MSYQL_INSTANCE} ]]; then diff --git a/terraform-tests/sqs_test.go b/terraform-tests/sqs_test.go new file mode 100644 index 000000000..dda7f809d --- /dev/null +++ b/terraform-tests/sqs_test.go @@ -0,0 +1,65 @@ +package terraformtests + +import ( + . "csbbrokerpakaws/terraform-tests/helpers" + "fmt" + "path" + "time" + + tfjson "github.com/hashicorp/terraform-json" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gstruct" +) + +var _ = Describe("SQS", Label("SQS-terraform"), Ordered, func() { + var ( + name string + plan tfjson.Plan + terraformProvisionDir string + defaultVars map[string]any + ) + + BeforeAll(func() { + name = fmt.Sprintf("csb-tf-test-sqs-%d-%d", GinkgoRandomSeed(), time.Now().Unix()) + + terraformProvisionDir = path.Join(workingDir, "sqs/provision") + Init(terraformProvisionDir) + }) + + BeforeEach(func() { + defaultVars = map[string]any{ + "instance_name": name, + "labels": map[string]string{"label1": "value1"}, + "aws_access_key_id": awsAccessKeyID, + "aws_secret_access_key": awsSecretAccessKey, + "region": awsRegion, + } + }) + + Context("with default values", func() { + BeforeAll(func() { + plan = ShowPlan(terraformProvisionDir, buildVars(defaultVars, map[string]any{})) + }) + + It("should create the right resources", func() { + Expect(plan.ResourceChanges).To(HaveLen(1)) + + Expect(ResourceChangesTypes(plan)).To(ConsistOf( + "aws_sqs_queue", + )) + }) + + It("should create an SQS queue with the correct properties", func() { + Expect(AfterValuesForType(plan, "aws_sqs_queue")).To( + MatchKeys(IgnoreExtras, Keys{ + "name": Equal(name), + "fifo_queue": BeFalse(), + "tags": MatchAllKeys(Keys{ + "label1": Equal("value1"), + }), + }), + ) + }) + }) +}) diff --git a/terraform/sqs/provision/main.tf b/terraform/sqs/provision/main.tf new file mode 100644 index 000000000..234c34375 --- /dev/null +++ b/terraform/sqs/provision/main.tf @@ -0,0 +1,5 @@ +resource "aws_sqs_queue" "queue" { + name = var.instance_name + fifo_queue = false + tags = var.labels +} \ No newline at end of file diff --git a/terraform/sqs/provision/outputs.tf b/terraform/sqs/provision/outputs.tf new file mode 100644 index 000000000..074eaa553 --- /dev/null +++ b/terraform/sqs/provision/outputs.tf @@ -0,0 +1,4 @@ +output "arn" { value = aws_sqs_queue.queue.arn } +output "url" { value = aws_sqs_queue.queue.id } +output "name" { value = aws_sqs_queue.queue.name } +output "region" { value = var.region } \ No newline at end of file diff --git a/terraform/sqs/provision/providers.tf b/terraform/sqs/provision/providers.tf new file mode 100644 index 000000000..46361a1f4 --- /dev/null +++ b/terraform/sqs/provision/providers.tf @@ -0,0 +1,5 @@ +provider "aws" { + region = var.region + access_key = var.aws_access_key_id + secret_key = var.aws_secret_access_key +} \ No newline at end of file diff --git a/terraform/sqs/provision/variables.tf b/terraform/sqs/provision/variables.tf new file mode 100644 index 000000000..9c16f3ad6 --- /dev/null +++ b/terraform/sqs/provision/variables.tf @@ -0,0 +1,12 @@ +variable "aws_access_key_id" { + type = string + sensitive = true +} +variable "aws_secret_access_key" { + type = string + sensitive = true +} +variable "region" { type = string } + +variable "instance_name" { type = string } +variable "labels" { type = map(any) } diff --git a/terraform/sqs/provision/versions.tf b/terraform/sqs/provision/versions.tf new file mode 100644 index 000000000..ba7d29e77 --- /dev/null +++ b/terraform/sqs/provision/versions.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } +} \ No newline at end of file