diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index ea039a28bc9..93528038444 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -67,6 +67,8 @@ jobs: build --define worker2_id=worker2 build --define worker2_public_api_target=worker2.example.com:8443 build --define mc_name=measurementConsumers/foo + build --define mc_api_key=foo + build --define mc_cert_name=measurementConsumers/foo/certificates/bar build --define edp1_name=dataProviders/foo1 build --define edp1_cert_name=dataProviders/foo1/certificates/bar1 build --define edp2_name=dataProviders/foo2 diff --git a/.github/workflows/run-k8s-tests.yml b/.github/workflows/run-k8s-tests.yml index 0f6ccea3c86..5906348104b 100644 --- a/.github/workflows/run-k8s-tests.yml +++ b/.github/workflows/run-k8s-tests.yml @@ -52,6 +52,7 @@ jobs: MC_NAME: ${{ vars.MC_NAME }} MC_API_KEY: ${{ secrets.MC_API_KEY }} GCLOUD_PROJECT: ${{ vars.GCLOUD_PROJECT }} + REPORTING_PUBLIC_API_TARGET: ${{ vars.REPORTING_PUBLIC_API_TARGET }} run: | cat << EOF > ~/.bazelrc common --config=ci @@ -59,6 +60,7 @@ jobs: build --define mc_name=$MC_NAME build --define mc_api_key=$MC_API_KEY build --define google_cloud_project=$GCLOUD_PROJECT + build --define reporting_public_api_target=$REPORTING_PUBLIC_API_TARGET test --test_output=streamed test --test_timeout=3600 EOF diff --git a/docs/gke/correctness-test.md b/docs/gke/correctness-test.md index 5b738737809..055def71ac0 100644 --- a/docs/gke/correctness-test.md +++ b/docs/gke/correctness-test.md @@ -80,7 +80,8 @@ bazel test //src/test/kotlin/org/wfanet/measurement/integration/k8s:SyntheticGen --test_output=streamed \ --define=kingdom_public_api_target=v2alpha.kingdom.dev.halo-cmm.org:8443 \ --define=mc_name=measurementConsumers/Rcn7fKd25C8 \ ---define=mc_api_key=W9q4zad246g +--define=mc_api_key=W9q4zad246g \ +--define=reporting_public_api_target=v2alpha.reporting.dev.halo-cmm.org:8443 ``` The time the test takes depends on the size of the data set. With the default diff --git a/src/main/k8s/local/testing/BUILD.bazel b/src/main/k8s/local/testing/BUILD.bazel index 51b188a2f9d..77a3f9dc270 100644 --- a/src/main/k8s/local/testing/BUILD.bazel +++ b/src/main/k8s/local/testing/BUILD.bazel @@ -63,10 +63,21 @@ kustomization_dir( "config_files_kustomization.yaml", "//src/main/k8s/testing/data:synthetic_generation_specs_small", "//src/main/k8s/testing/secretfiles:known_event_group_metadata_type_set", + "//src/main/k8s/testing/secretfiles:metric_spec_config.textproto", ], renames = {"config_files_kustomization.yaml": "kustomization.yaml"}, ) +kustomization_dir( + name = "config_files_for_panel_match", + srcs = [ + "config_files_for_panel_match_kustomization.yaml", + "//src/main/k8s/testing/data:synthetic_generation_specs_small", + "//src/main/k8s/testing/secretfiles:known_event_group_metadata_type_set", + ], + renames = {"config_files_for_panel_match_kustomization.yaml": "kustomization.yaml"}, +) + kustomization_dir( name = "db_creds", srcs = ["db_creds_kustomization.yaml"], @@ -75,6 +86,15 @@ kustomization_dir( }, ) +kustomization_dir( + name = "mc_config", + srcs = ["mc_config_kustomization.yaml"], + renames = { + "mc_config_kustomization.yaml": "kustomization.yaml", + }, + tags = ["manual"], +) + kustomization_dir( name = "cmms", srcs = [ @@ -83,12 +103,14 @@ kustomization_dir( "//src/main/k8s/local:emulators", "//src/main/k8s/local:kingdom", "//src/main/k8s/local:postgres_database", + "//src/main/k8s/local:reporting_v2", ], generate_kustomization = True, tags = ["manual"], deps = [ ":config_files", ":db_creds", + ":mc_config", "//src/main/k8s/testing/secretfiles:kustomization", ], ) @@ -103,7 +125,7 @@ kustomization_dir( generate_kustomization = True, tags = ["manual"], deps = [ - ":config_files", + ":config_files_for_panel_match", "//src/main/k8s/testing/secretfiles:kustomization", ], ) diff --git a/src/main/k8s/local/testing/config_files_for_panel_match_kustomization.yaml b/src/main/k8s/local/testing/config_files_for_panel_match_kustomization.yaml new file mode 100644 index 00000000000..364ed73ac20 --- /dev/null +++ b/src/main/k8s/local/testing/config_files_for_panel_match_kustomization.yaml @@ -0,0 +1,22 @@ +# Copyright 2024 The Cross-Media Measurement Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +configMapGenerator: +- name: config-files-for-panel-match + files: + - authority_key_identifier_to_principal_map.textproto + - synthetic_population_spec_small.textproto + - synthetic_event_group_spec_small_1.textproto + - synthetic_event_group_spec_small_2.textproto + - known_event_group_metadata_type_set.pb diff --git a/src/main/k8s/local/testing/config_files_kustomization.yaml b/src/main/k8s/local/testing/config_files_kustomization.yaml index d882167e307..71c188ba7fb 100644 --- a/src/main/k8s/local/testing/config_files_kustomization.yaml +++ b/src/main/k8s/local/testing/config_files_kustomization.yaml @@ -16,6 +16,8 @@ configMapGenerator: - name: config-files files: - authority_key_identifier_to_principal_map.textproto + - encryption_key_pair_config.textproto + - metric_spec_config.textproto - synthetic_population_spec_small.textproto - synthetic_event_group_spec_small_1.textproto - synthetic_event_group_spec_small_2.textproto diff --git a/src/main/k8s/local/testing/mc_config_kustomization.yaml b/src/main/k8s/local/testing/mc_config_kustomization.yaml new file mode 100644 index 00000000000..8e329fcff4d --- /dev/null +++ b/src/main/k8s/local/testing/mc_config_kustomization.yaml @@ -0,0 +1,18 @@ +# Copyright 2024 The Cross-Media Measurement Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +secretGenerator: +- name: mc-config + files: + - measurement_consumer_config.textproto diff --git a/src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/BUILD.bazel b/src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/BUILD.bazel index 2865c224525..15694208ce0 100644 --- a/src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/BUILD.bazel +++ b/src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/BUILD.bazel @@ -8,6 +8,7 @@ package(default_visibility = [ "//src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/testing:__pkg__", "//src/main/kotlin/org/wfanet/measurement/loadtest/panelmatch:__pkg__", "//src/main/kotlin/org/wfanet/measurement/loadtest/panelmatchresourcesetup:__pkg__", + "//src/main/kotlin/org/wfanet/measurement/loadtest/reporting:__pkg__", "//src/main/kotlin/org/wfanet/measurement/loadtest/resourcesetup:__pkg__", "//src/test/kotlin/org/wfanet/measurement/integration/common:__pkg__", ]) diff --git a/src/main/kotlin/org/wfanet/measurement/loadtest/reporting/BUILD.bazel b/src/main/kotlin/org/wfanet/measurement/loadtest/reporting/BUILD.bazel new file mode 100644 index 00000000000..962beda9818 --- /dev/null +++ b/src/main/kotlin/org/wfanet/measurement/loadtest/reporting/BUILD.bazel @@ -0,0 +1,26 @@ +load("@wfa_rules_kotlin_jvm//kotlin:defs.bzl", "kt_jvm_library") + +package( + default_testonly = True, + default_visibility = [ + "//src/main/kotlin/org/wfanet/measurement/integration:__subpackages__", + "//src/main/kotlin/org/wfanet/measurement/loadtest:__subpackages__", + "//src/test/kotlin/org/wfanet/measurement/integration:__subpackages__", + "//src/test/kotlin/org/wfanet/measurement/loadtest:__subpackages__", + ], +) + +kt_jvm_library( + name = "simulator", + srcs = ["ReportingUserSimulator.kt"], + deps = [ + "//src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha:data_providers_service", + "//src/main/kotlin/org/wfanet/measurement/loadtest/config:test_identifiers", + "//src/main/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha:event_groups_service", + "//src/main/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha:metric_calculation_specs_service", + "//src/main/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha:reporting_sets_service", + "//src/main/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha:reports_service", + "//src/main/proto/wfa/measurement/api/v2alpha/event_templates/testing:test_event_kt_jvm_proto", + "@wfa_common_jvm//imports/java/com/google/common/truth/extensions/proto", + ], +) diff --git a/src/main/kotlin/org/wfanet/measurement/loadtest/reporting/ReportingUserSimulator.kt b/src/main/kotlin/org/wfanet/measurement/loadtest/reporting/ReportingUserSimulator.kt new file mode 100644 index 00000000000..b27c4f0c47e --- /dev/null +++ b/src/main/kotlin/org/wfanet/measurement/loadtest/reporting/ReportingUserSimulator.kt @@ -0,0 +1,235 @@ +/* + * Copyright 2024 The Cross-Media Measurement Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.wfanet.measurement.loadtest.reporting + +import com.google.common.truth.Truth.assertThat +import com.google.type.DayOfWeek +import com.google.type.date +import com.google.type.dateTime +import com.google.type.timeZone +import io.grpc.StatusException +import java.util.logging.Logger +import kotlinx.coroutines.delay +import org.wfanet.measurement.api.v2alpha.DataProvider +import org.wfanet.measurement.api.v2alpha.DataProvidersGrpcKt +import org.wfanet.measurement.api.v2alpha.getDataProviderRequest +import org.wfanet.measurement.loadtest.config.TestIdentifiers +import org.wfanet.measurement.reporting.v2alpha.EventGroup +import org.wfanet.measurement.reporting.v2alpha.EventGroupsGrpcKt +import org.wfanet.measurement.reporting.v2alpha.ListEventGroupsResponse +import org.wfanet.measurement.reporting.v2alpha.MetricCalculationSpec +import org.wfanet.measurement.reporting.v2alpha.MetricCalculationSpecKt +import org.wfanet.measurement.reporting.v2alpha.MetricCalculationSpecsGrpcKt +import org.wfanet.measurement.reporting.v2alpha.MetricSpecKt +import org.wfanet.measurement.reporting.v2alpha.Report +import org.wfanet.measurement.reporting.v2alpha.ReportKt +import org.wfanet.measurement.reporting.v2alpha.ReportingSet +import org.wfanet.measurement.reporting.v2alpha.ReportingSetKt +import org.wfanet.measurement.reporting.v2alpha.ReportingSetsGrpcKt +import org.wfanet.measurement.reporting.v2alpha.ReportsGrpcKt +import org.wfanet.measurement.reporting.v2alpha.createMetricCalculationSpecRequest +import org.wfanet.measurement.reporting.v2alpha.createReportRequest +import org.wfanet.measurement.reporting.v2alpha.createReportingSetRequest +import org.wfanet.measurement.reporting.v2alpha.getReportRequest +import org.wfanet.measurement.reporting.v2alpha.listEventGroupsRequest +import org.wfanet.measurement.reporting.v2alpha.metricCalculationSpec +import org.wfanet.measurement.reporting.v2alpha.metricSpec +import org.wfanet.measurement.reporting.v2alpha.report +import org.wfanet.measurement.reporting.v2alpha.reportingSet + +/** Simulator for Reporting operations on the Reporting public API. */ +class ReportingUserSimulator( + private val measurementConsumerName: String, + private val dataProvidersClient: DataProvidersGrpcKt.DataProvidersCoroutineStub, + private val eventGroupsClient: EventGroupsGrpcKt.EventGroupsCoroutineStub, + private val reportingSetsClient: ReportingSetsGrpcKt.ReportingSetsCoroutineStub, + private val metricCalculationSpecsClient: + MetricCalculationSpecsGrpcKt.MetricCalculationSpecsCoroutineStub, + private val reportsClient: ReportsGrpcKt.ReportsCoroutineStub, +) { + suspend fun testCreateReport(runId: String) { + logger.info("Creating report...") + + val eventGroup = + listEventGroups() + .filter { + it.eventGroupReferenceId.startsWith( + TestIdentifiers.SIMULATOR_EVENT_GROUP_REFERENCE_ID_PREFIX + ) + } + .firstOrNull { + getDataProvider(it.cmmsDataProvider).capabilities.honestMajorityShareShuffleSupported + } ?: listEventGroups().first() + val createdPrimitiveReportingSet = createPrimitiveReportingSet(eventGroup) + val createdMetricCalculationSpec = createMetricCalculationSpec() + + val report = report { + reportingMetricEntries += + ReportKt.reportingMetricEntry { + key = createdPrimitiveReportingSet.name + value = + ReportKt.reportingMetricCalculationSpec { + metricCalculationSpecs += createdMetricCalculationSpec.name + } + } + reportingInterval = + ReportKt.reportingInterval { + reportStart = dateTime { + year = 2024 + month = 1 + day = 3 + timeZone = timeZone { id = "America/Los_Angeles" } + } + reportEnd = date { + year = 2024 + month = 1 + day = 4 + } + } + } + + val createdReport = + try { + reportsClient.createReport( + createReportRequest { + parent = measurementConsumerName + this.report = report + reportId = "a-$runId" + } + ) + } catch (e: StatusException) { + throw Exception("Error creating Report", e) + } + + val completedReport = pollForCompletedReport(createdReport.name) + + assertThat(completedReport.state).isEqualTo(Report.State.SUCCEEDED) + logger.info("Report creation succeeded") + } + + private suspend fun listEventGroups(): List { + try { + return buildList { + var response: ListEventGroupsResponse = ListEventGroupsResponse.getDefaultInstance() + do { + response = + eventGroupsClient.listEventGroups( + listEventGroupsRequest { + parent = measurementConsumerName + pageSize = 1000 + pageToken = response.nextPageToken + } + ) + addAll(response.eventGroupsList) + } while (response.nextPageToken.isNotEmpty()) + } + } catch (e: StatusException) { + throw Exception("Error listing EventGroups", e) + } + } + + private suspend fun getDataProvider(dataProviderName: String): DataProvider { + try { + return dataProvidersClient.getDataProvider(getDataProviderRequest { name = dataProviderName }) + } catch (e: StatusException) { + throw Exception("Error getting DataProvider $dataProviderName", e) + } + } + + private suspend fun createPrimitiveReportingSet(eventGroup: EventGroup): ReportingSet { + val primitiveReportingSet = reportingSet { + primitive = ReportingSetKt.primitive { cmmsEventGroups += eventGroup.cmmsEventGroup } + } + + try { + return reportingSetsClient.createReportingSet( + createReportingSetRequest { + parent = measurementConsumerName + reportingSet = primitiveReportingSet + reportingSetId = "a-123" + } + ) + } catch (e: StatusException) { + throw Exception("Error creating ReportingSet", e) + } + } + + private suspend fun createMetricCalculationSpec(): MetricCalculationSpec { + try { + return metricCalculationSpecsClient.createMetricCalculationSpec( + createMetricCalculationSpecRequest { + parent = measurementConsumerName + metricCalculationSpecId = "a-123" + metricCalculationSpec = metricCalculationSpec { + displayName = "union reach" + metricSpecs += metricSpec { + reach = + MetricSpecKt.reachParams { + singleDataProviderParams = + MetricSpecKt.samplingAndPrivacyParams { + privacyParams = MetricSpecKt.differentialPrivacyParams {} + } + multipleDataProviderParams = + MetricSpecKt.samplingAndPrivacyParams { + privacyParams = MetricSpecKt.differentialPrivacyParams {} + } + } + } + metricFrequencySpec = + MetricCalculationSpecKt.metricFrequencySpec { + weekly = + MetricCalculationSpecKt.MetricFrequencySpecKt.weekly { + dayOfWeek = DayOfWeek.WEDNESDAY + } + } + trailingWindow = + MetricCalculationSpecKt.trailingWindow { + count = 1 + increment = MetricCalculationSpec.TrailingWindow.Increment.WEEK + } + } + } + ) + } catch (e: StatusException) { + throw Exception("Error creating MetricCalculationSpec", e) + } + } + + private suspend fun pollForCompletedReport(reportName: String): Report { + while (true) { + val retrievedReport = + try { + reportsClient.getReport(getReportRequest { name = reportName }) + } catch (e: StatusException) { + throw Exception("Error getting Report", e) + } + + @Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA") // Proto enum fields are never null. + when (retrievedReport.state) { + Report.State.SUCCEEDED, + Report.State.FAILED -> return retrievedReport + Report.State.RUNNING, + Report.State.UNRECOGNIZED, + Report.State.STATE_UNSPECIFIED -> delay(5000) + } + } + } + + companion object { + private val logger: Logger = Logger.getLogger(this::class.java.name) + } +} diff --git a/src/main/kotlin/org/wfanet/measurement/loadtest/resourcesetup/BUILD.bazel b/src/main/kotlin/org/wfanet/measurement/loadtest/resourcesetup/BUILD.bazel index f4a374643f5..94955ed1f51 100644 --- a/src/main/kotlin/org/wfanet/measurement/loadtest/resourcesetup/BUILD.bazel +++ b/src/main/kotlin/org/wfanet/measurement/loadtest/resourcesetup/BUILD.bazel @@ -36,6 +36,8 @@ kt_jvm_library( "//src/main/proto/wfa/measurement/api/v2alpha:measurement_consumer_kt_jvm_proto", "//src/main/proto/wfa/measurement/api/v2alpha:measurement_consumers_service_kt_jvm_grpc_proto", "//src/main/proto/wfa/measurement/config:authority_key_to_principal_map_kt_jvm_proto", + "//src/main/proto/wfa/measurement/config/reporting:encryption_key_pair_config_kt_jvm_proto", + "//src/main/proto/wfa/measurement/config/reporting:measurement_consumer_config_kt_jvm_proto", "//src/main/proto/wfa/measurement/internal/kingdom:account_kt_jvm_proto", "//src/main/proto/wfa/measurement/internal/kingdom:accounts_service_kt_jvm_grpc_proto", "//src/main/proto/wfa/measurement/internal/kingdom:certificate_kt_jvm_proto", diff --git a/src/main/kotlin/org/wfanet/measurement/loadtest/resourcesetup/ResourceSetup.kt b/src/main/kotlin/org/wfanet/measurement/loadtest/resourcesetup/ResourceSetup.kt index 4bae0c8d911..4123f6bb86b 100644 --- a/src/main/kotlin/org/wfanet/measurement/loadtest/resourcesetup/ResourceSetup.kt +++ b/src/main/kotlin/org/wfanet/measurement/loadtest/resourcesetup/ResourceSetup.kt @@ -55,6 +55,10 @@ import org.wfanet.measurement.common.crypto.tink.SelfIssuedIdTokens.generateIdTo import org.wfanet.measurement.common.identity.externalIdToApiId import org.wfanet.measurement.config.AuthorityKeyToPrincipalMapKt import org.wfanet.measurement.config.authorityKeyToPrincipalMap +import org.wfanet.measurement.config.reporting.EncryptionKeyPairConfigKt +import org.wfanet.measurement.config.reporting.encryptionKeyPairConfig +import org.wfanet.measurement.config.reporting.measurementConsumerConfig +import org.wfanet.measurement.config.reporting.measurementConsumerConfigs import org.wfanet.measurement.consent.client.measurementconsumer.signEncryptionPublicKey import org.wfanet.measurement.internal.kingdom.Account as InternalAccount import org.wfanet.measurement.internal.kingdom.AccountsGrpcKt @@ -213,6 +217,47 @@ class ResourceSetup( TextFormat.printer().print(akidMap, writer) } + val measurementConsumerConfig = measurementConsumerConfigs { + for (resource in resources) { + when (resource.resourceCase) { + Resources.Resource.ResourceCase.MEASUREMENT_CONSUMER -> + configs.put( + resource.name, + measurementConsumerConfig { + apiKey = resource.measurementConsumer.apiKey + signingCertificateName = resource.measurementConsumer.certificate + signingPrivateKeyPath = MEASUREMENT_CONSUMER_SIGNING_PRIVATE_KEY_PATH + }, + ) + else -> continue + } + } + } + output.resolve(MEASUREMENT_CONSUMER_CONFIG_FILE).writer().use { writer -> + TextFormat.printer().print(measurementConsumerConfig, writer) + } + + val encryptionKeyPairConfig = encryptionKeyPairConfig { + for (resource in resources) { + when (resource.resourceCase) { + Resources.Resource.ResourceCase.MEASUREMENT_CONSUMER -> + principalKeyPairs += + EncryptionKeyPairConfigKt.principalKeyPairs { + principal = resource.name + keyPairs += + EncryptionKeyPairConfigKt.keyPair { + publicKeyFile = MEASUREMENT_CONSUMER_ENCRYPTION_PUBLIC_KEY_PATH + privateKeyFile = MEASUREMENT_CONSUMER_ENCRYPTION_PRIVATE_KEY_PATH + } + } + else -> continue + } + } + } + output.resolve(ENCRYPTION_KEY_PAIR_CONFIG_FILE).writer().use { writer -> + TextFormat.printer().print(encryptionKeyPairConfig, writer) + } + val configName = bazelConfigName output.resolve(BAZEL_RC_FILE).writer().use { writer -> for (resource in resources) { @@ -413,6 +458,11 @@ class ResourceSetup( const val RESOURCES_OUTPUT_FILE = "resources.textproto" const val AKID_PRINCIPAL_MAP_FILE = "authority_key_identifier_to_principal_map.textproto" const val BAZEL_RC_FILE = "resource-setup.bazelrc" + const val MEASUREMENT_CONSUMER_CONFIG_FILE = "measurement_consumer_config.textproto" + const val ENCRYPTION_KEY_PAIR_CONFIG_FILE = "encryption_key_pair_config.textproto" + const val MEASUREMENT_CONSUMER_SIGNING_PRIVATE_KEY_PATH = "mc_cs_private.der" + const val MEASUREMENT_CONSUMER_ENCRYPTION_PUBLIC_KEY_PATH = "mc_enc_public.tink" + const val MEASUREMENT_CONSUMER_ENCRYPTION_PRIVATE_KEY_PATH = "mc_enc_private.tink" private val logger: Logger = Logger.getLogger(this::class.java.name) } diff --git a/src/main/proto/wfa/measurement/integration/k8s/testing/correctness_test_config.proto b/src/main/proto/wfa/measurement/integration/k8s/testing/correctness_test_config.proto index 4c894b4ca4f..25b14b56827 100644 --- a/src/main/proto/wfa/measurement/integration/k8s/testing/correctness_test_config.proto +++ b/src/main/proto/wfa/measurement/integration/k8s/testing/correctness_test_config.proto @@ -38,4 +38,13 @@ message CorrectnessTestConfig { // Authentication key for the CMMS public API. string api_authentication_key = 4; + + // gRPC target of Reporting public API server. + string reporting_public_api_target = 5; + + // Expected hostname (DNS-ID) in the reporting public API server's TLS + // certificate. + // + // If not specified, standard TLS DNS-ID derivation will be used. + string reporting_public_api_cert_host = 6; } diff --git a/src/test/kotlin/org/wfanet/measurement/integration/k8s/AbstractCorrectnessTest.kt b/src/test/kotlin/org/wfanet/measurement/integration/k8s/AbstractCorrectnessTest.kt index f4a28f4d1cd..64ed27e3e9b 100644 --- a/src/test/kotlin/org/wfanet/measurement/integration/k8s/AbstractCorrectnessTest.kt +++ b/src/test/kotlin/org/wfanet/measurement/integration/k8s/AbstractCorrectnessTest.kt @@ -28,6 +28,7 @@ import org.wfanet.measurement.common.crypto.SigningKeyHandle import org.wfanet.measurement.integration.common.loadEncryptionPrivateKey import org.wfanet.measurement.integration.common.loadSigningKey import org.wfanet.measurement.loadtest.measurementconsumer.MeasurementConsumerSimulator +import org.wfanet.measurement.loadtest.reporting.ReportingUserSimulator /** Test for correctness of the CMMS on Kubernetes. */ abstract class AbstractCorrectnessTest(private val measurementSystem: MeasurementSystem) { @@ -37,6 +38,9 @@ abstract class AbstractCorrectnessTest(private val measurementSystem: Measuremen private val testHarness: MeasurementConsumerSimulator get() = measurementSystem.testHarness + private val reportingTestHarness: ReportingUserSimulator + get() = measurementSystem.reportingTestHarness + @Test(timeout = 1 * 60 * 1000) fun `impression measurement completes with expected result`() = runBlocking { testHarness.testImpression("$runId-impression") @@ -63,9 +67,15 @@ abstract class AbstractCorrectnessTest(private val measurementSystem: Measuremen ) } + @Test(timeout = 1 * 60 * 1000) + fun `report can be created`() = runBlocking { + reportingTestHarness.testCreateReport("$runId-test-report") + } + interface MeasurementSystem { val runId: String val testHarness: MeasurementConsumerSimulator + val reportingTestHarness: ReportingUserSimulator } companion object { @@ -97,6 +107,14 @@ abstract class AbstractCorrectnessTest(private val measurementSystem: Measuremen SigningCerts.fromPemFiles(cert, key, trustedCerts) } + val REPORTING_SIGNING_CERTS: SigningCerts by lazy { + val secretFiles = getRuntimePath(SECRET_FILES_PATH) + val trustedCerts = secretFiles.resolve("reporting_root.pem").toFile() + val cert = secretFiles.resolve("mc_tls.pem").toFile() + val key = secretFiles.resolve("mc_tls.key").toFile() + SigningCerts.fromPemFiles(cert, key, trustedCerts) + } + val MC_ENCRYPTION_PRIVATE_KEY: PrivateKeyHandle by lazy { loadEncryptionPrivateKey(MC_ENCRYPTION_PRIVATE_KEY_NAME) } diff --git a/src/test/kotlin/org/wfanet/measurement/integration/k8s/BUILD.bazel b/src/test/kotlin/org/wfanet/measurement/integration/k8s/BUILD.bazel index 3883661dbdb..2f0097c1e6f 100644 --- a/src/test/kotlin/org/wfanet/measurement/integration/k8s/BUILD.bazel +++ b/src/test/kotlin/org/wfanet/measurement/integration/k8s/BUILD.bazel @@ -13,11 +13,13 @@ kt_jvm_library( srcs = ["AbstractCorrectnessTest.kt"], data = [ "//src/main/k8s/testing/secretfiles:mc_trusted_certs.pem", + "//src/main/k8s/testing/secretfiles:reporting_root.pem", "//src/main/k8s/testing/secretfiles:secret_files", ], deps = [ "//src/main/kotlin/org/wfanet/measurement/integration/common:configs", "//src/main/kotlin/org/wfanet/measurement/loadtest/measurementconsumer:simulator", + "//src/main/kotlin/org/wfanet/measurement/loadtest/reporting:simulator", "@wfa_common_jvm//imports/java/com/google/common/truth", "@wfa_common_jvm//imports/java/org/junit", "@wfa_common_jvm//imports/kotlin/kotlinx/coroutines:core", @@ -58,6 +60,7 @@ kt_jvm_library( "//src/main/kotlin/org/wfanet/measurement/integration/common:synthetic_generation_specs", "//src/main/kotlin/org/wfanet/measurement/loadtest/config:vid_sampling", "//src/main/kotlin/org/wfanet/measurement/loadtest/measurementconsumer:synthetic_generator_event_query", + "//src/main/kotlin/org/wfanet/measurement/loadtest/reporting:simulator", "//src/main/proto/wfa/measurement/integration/k8s/testing:correctness_test_config_kt_jvm_proto", "@wfa_common_jvm//imports/java/org/junit", "@wfa_common_jvm//imports/kotlin/kotlinx/coroutines:core", @@ -74,6 +77,8 @@ expand_template( "{kingdom_public_api_cert_host}": "localhost", "{mc_name}": TEST_K8S_SETTINGS.mc_name, "{mc_api_key}": TEST_K8S_SETTINGS.mc_api_key, + "{reporting_public_api_target}": "$(reporting_public_api_target)", + "{reporting_public_api_cert_host}": "localhost", }, tags = ["manual"], template = "correctness_test_config.tmpl.textproto", diff --git a/src/test/kotlin/org/wfanet/measurement/integration/k8s/EmptyClusterCorrectnessTest.kt b/src/test/kotlin/org/wfanet/measurement/integration/k8s/EmptyClusterCorrectnessTest.kt index 0ddd08ec41b..95f24dc4307 100644 --- a/src/test/kotlin/org/wfanet/measurement/integration/k8s/EmptyClusterCorrectnessTest.kt +++ b/src/test/kotlin/org/wfanet/measurement/integration/k8s/EmptyClusterCorrectnessTest.kt @@ -69,10 +69,14 @@ import org.wfanet.measurement.internal.kingdom.AccountsGrpcKt import org.wfanet.measurement.loadtest.measurementconsumer.MeasurementConsumerData import org.wfanet.measurement.loadtest.measurementconsumer.MeasurementConsumerSimulator import org.wfanet.measurement.loadtest.measurementconsumer.MetadataSyntheticGeneratorEventQuery +import org.wfanet.measurement.loadtest.reporting.ReportingUserSimulator import org.wfanet.measurement.loadtest.resourcesetup.DuchyCert import org.wfanet.measurement.loadtest.resourcesetup.EntityContent import org.wfanet.measurement.loadtest.resourcesetup.ResourceSetup import org.wfanet.measurement.loadtest.resourcesetup.Resources +import org.wfanet.measurement.reporting.v2alpha.MetricCalculationSpecsGrpcKt +import org.wfanet.measurement.reporting.v2alpha.ReportingSetsGrpcKt +import org.wfanet.measurement.reporting.v2alpha.ReportsGrpcKt /** * Test for correctness of the CMMS on a single "empty" Kubernetes cluster using the `local` @@ -108,6 +112,7 @@ class EmptyClusterCorrectnessTest : AbstractCorrectnessTest(measurementSystem) { val worker1Cert: String, val worker2Cert: String, val measurementConsumer: String, + val measurementConsumerCert: String, val apiKey: String, val dataProviders: Map, ) { @@ -117,6 +122,7 @@ class EmptyClusterCorrectnessTest : AbstractCorrectnessTest(measurementSystem) { var worker1Cert: String? = null var worker2Cert: String? = null var measurementConsumer: String? = null + var measurementConsumerCert: String? = null var apiKey: String? = null val dataProviders = mutableMapOf() @@ -126,6 +132,7 @@ class EmptyClusterCorrectnessTest : AbstractCorrectnessTest(measurementSystem) { Resources.Resource.ResourceCase.MEASUREMENT_CONSUMER -> { measurementConsumer = resource.name apiKey = resource.measurementConsumer.apiKey + measurementConsumerCert = resource.measurementConsumer.certificate } Resources.Resource.ResourceCase.DATA_PROVIDER -> { val displayName = resource.dataProvider.displayName @@ -146,12 +153,13 @@ class EmptyClusterCorrectnessTest : AbstractCorrectnessTest(measurementSystem) { } return ResourceInfo( - requireNotNull(aggregatorCert), - requireNotNull(worker1Cert), - requireNotNull(worker2Cert), - requireNotNull(measurementConsumer), - requireNotNull(apiKey), - dataProviders, + aggregatorCert = requireNotNull(aggregatorCert), + worker1Cert = requireNotNull(worker1Cert), + worker2Cert = requireNotNull(worker2Cert), + measurementConsumer = requireNotNull(measurementConsumer), + measurementConsumerCert = requireNotNull(measurementConsumerCert), + apiKey = requireNotNull(apiKey), + dataProviders = dataProviders, ) } } @@ -174,6 +182,10 @@ class EmptyClusterCorrectnessTest : AbstractCorrectnessTest(measurementSystem) { override val testHarness: MeasurementConsumerSimulator get() = _testHarness + private lateinit var _reportingTestHarness: ReportingUserSimulator + override val reportingTestHarness: ReportingUserSimulator + get() = _reportingTestHarness + override fun apply(base: Statement, description: Description): Statement { return object : Statement() { override fun evaluate() { @@ -182,6 +194,7 @@ class EmptyClusterCorrectnessTest : AbstractCorrectnessTest(measurementSystem) { withTimeout(Duration.ofMinutes(5)) { val measurementConsumerData = populateCluster() _testHarness = createTestHarness(measurementConsumerData) + _reportingTestHarness = createReportingUserSimulator(measurementConsumerData) } } base.evaluate() @@ -212,7 +225,12 @@ class EmptyClusterCorrectnessTest : AbstractCorrectnessTest(measurementSystem) { val resourceSetupOutput = runResourceSetup(duchyCerts, edpEntityContents, measurementConsumerContent) val resourceInfo = ResourceInfo.from(resourceSetupOutput.resources) - loadFullCmms(resourceInfo, resourceSetupOutput.akidPrincipalMap) + loadFullCmms( + resourceInfo, + resourceSetupOutput.akidPrincipalMap, + resourceSetupOutput.measurementConsumerConfig, + resourceSetupOutput.encryptionKeyPairConfig, + ) val encryptionPrivateKey: TinkPrivateKeyHandle = withContext(Dispatchers.IO) { @@ -259,6 +277,35 @@ class EmptyClusterCorrectnessTest : AbstractCorrectnessTest(measurementSystem) { ) } + private suspend fun createReportingUserSimulator( + measurementConsumerData: MeasurementConsumerData + ): ReportingUserSimulator { + val reportingPublicPod: V1Pod = getPod(REPORTING_PUBLIC_DEPLOYMENT_NAME) + + val publicApiForwarder = PortForwarder(reportingPublicPod, SERVER_PORT) + portForwarders.add(publicApiForwarder) + + val publicApiAddress: InetSocketAddress = + withContext(Dispatchers.IO) { publicApiForwarder.start() } + val publicApiChannel: Channel = + buildMutualTlsChannel(publicApiAddress.toTarget(), REPORTING_SIGNING_CERTS) + .also { channels.add(it) } + .withDefaultDeadline(DEFAULT_RPC_DEADLINE) + + return ReportingUserSimulator( + measurementConsumerName = measurementConsumerData.name, + dataProvidersClient = DataProvidersGrpcKt.DataProvidersCoroutineStub(publicApiChannel), + eventGroupsClient = + org.wfanet.measurement.reporting.v2alpha.EventGroupsGrpcKt.EventGroupsCoroutineStub( + publicApiChannel + ), + reportingSetsClient = ReportingSetsGrpcKt.ReportingSetsCoroutineStub(publicApiChannel), + metricCalculationSpecsClient = + MetricCalculationSpecsGrpcKt.MetricCalculationSpecsCoroutineStub(publicApiChannel), + reportsClient = ReportsGrpcKt.ReportsCoroutineStub(publicApiChannel), + ) + } + fun stopPortForwarding() { for (channel in channels) { channel.shutdown() @@ -268,7 +315,12 @@ class EmptyClusterCorrectnessTest : AbstractCorrectnessTest(measurementSystem) { } } - private suspend fun loadFullCmms(resourceInfo: ResourceInfo, akidPrincipalMap: File) { + private suspend fun loadFullCmms( + resourceInfo: ResourceInfo, + akidPrincipalMap: File, + measurementConsumerConfig: File, + encryptionKeyPairConfig: File, + ) { val appliedObjects: List = withContext(Dispatchers.IO) { val outputDir = tempDir.newFolder("cmms") @@ -278,6 +330,13 @@ class EmptyClusterCorrectnessTest : AbstractCorrectnessTest(measurementSystem) { logger.info("Copying $akidPrincipalMap to $CONFIG_FILES_PATH") akidPrincipalMap.copyTo(configFilesDir.resolve(akidPrincipalMap.name)) + logger.info("Copying $encryptionKeyPairConfig to $CONFIG_FILES_PATH") + encryptionKeyPairConfig.copyTo(configFilesDir.resolve(encryptionKeyPairConfig.name)) + + val mcConfigDir = outputDir.toPath().resolve(MC_CONFIG_PATH).toFile() + logger.info("Copying $measurementConsumerConfig to $MC_CONFIG_PATH") + measurementConsumerConfig.copyTo(mcConfigDir.resolve(measurementConsumerConfig.name)) + val configTemplate: File = outputDir.resolve("config.yaml") kustomize( outputDir.toPath().resolve(LOCAL_K8S_TESTING_PATH).resolve("cmms").toFile(), @@ -291,6 +350,8 @@ class EmptyClusterCorrectnessTest : AbstractCorrectnessTest(measurementSystem) { .replace("{worker1_cert_name}", resourceInfo.worker1Cert) .replace("{worker2_cert_name}", resourceInfo.worker2Cert) .replace("{mc_name}", resourceInfo.measurementConsumer) + .replace("{mc_api_key}", resourceInfo.apiKey) + .replace("{mc_cert_name}", resourceInfo.measurementConsumerCert) .let { var config = it for ((displayName, resource) in resourceInfo.dataProviders) { @@ -378,6 +439,8 @@ class EmptyClusterCorrectnessTest : AbstractCorrectnessTest(measurementSystem) { return ResourceSetupOutput( resources, outputDir.resolve(ResourceSetup.AKID_PRINCIPAL_MAP_FILE), + outputDir.resolve(ResourceSetup.MEASUREMENT_CONSUMER_CONFIG_FILE), + outputDir.resolve(ResourceSetup.ENCRYPTION_KEY_PAIR_CONFIG_FILE), ) } @@ -439,6 +502,8 @@ class EmptyClusterCorrectnessTest : AbstractCorrectnessTest(measurementSystem) { data class ResourceSetupOutput( val resources: List, val akidPrincipalMap: File, + val measurementConsumerConfig: File, + val encryptionKeyPairConfig: File, ) } @@ -456,6 +521,8 @@ class EmptyClusterCorrectnessTest : AbstractCorrectnessTest(measurementSystem) { private val DEFAULT_RPC_DEADLINE = Duration.ofSeconds(30) private const val KINGDOM_INTERNAL_DEPLOYMENT_NAME = "gcp-kingdom-data-server-deployment" private const val KINGDOM_PUBLIC_DEPLOYMENT_NAME = "v2alpha-public-api-server-deployment" + private const val REPORTING_PUBLIC_DEPLOYMENT_NAME = + "reporting-v2alpha-public-api-server-deployment" private const val NUM_DATA_PROVIDERS = 6 private val EDP_DISPLAY_NAMES: List = (1..NUM_DATA_PROVIDERS).map { "edp$it" } private val READY_TIMEOUT = Duration.ofMinutes(2L) @@ -463,6 +530,7 @@ class EmptyClusterCorrectnessTest : AbstractCorrectnessTest(measurementSystem) { private val LOCAL_K8S_PATH = Paths.get("src", "main", "k8s", "local") private val LOCAL_K8S_TESTING_PATH = LOCAL_K8S_PATH.resolve("testing") private val CONFIG_FILES_PATH = LOCAL_K8S_TESTING_PATH.resolve("config_files") + private val MC_CONFIG_PATH = LOCAL_K8S_TESTING_PATH.resolve("mc_config") private val IMAGE_PUSHER_PATH = Paths.get("src", "main", "docker", "push_all_local_images.bash") private val tempDir = TemporaryFolder() diff --git a/src/test/kotlin/org/wfanet/measurement/integration/k8s/SyntheticGeneratorCorrectnessTest.kt b/src/test/kotlin/org/wfanet/measurement/integration/k8s/SyntheticGeneratorCorrectnessTest.kt index 86608dd80fa..6108c68c62f 100644 --- a/src/test/kotlin/org/wfanet/measurement/integration/k8s/SyntheticGeneratorCorrectnessTest.kt +++ b/src/test/kotlin/org/wfanet/measurement/integration/k8s/SyntheticGeneratorCorrectnessTest.kt @@ -41,6 +41,9 @@ import org.wfanet.measurement.loadtest.dataprovider.SyntheticGeneratorEventQuery import org.wfanet.measurement.loadtest.measurementconsumer.MeasurementConsumerData import org.wfanet.measurement.loadtest.measurementconsumer.MeasurementConsumerSimulator import org.wfanet.measurement.loadtest.measurementconsumer.MetadataSyntheticGeneratorEventQuery +import org.wfanet.measurement.loadtest.reporting.ReportingUserSimulator +import org.wfanet.measurement.reporting.v2alpha.MetricCalculationSpecsGrpcKt +import org.wfanet.measurement.reporting.v2alpha.ReportsGrpcKt /** * Test for correctness of an existing CMMS on Kubernetes where the EDP simulators use @@ -48,16 +51,21 @@ import org.wfanet.measurement.loadtest.measurementconsumer.MetadataSyntheticGene * The computation composition is using ACDP by assumption. * * This currently assumes that the CMMS instance is using the certificates and keys from this Bazel - * workspace. + * workspace. It also assumes that there is a Reporting system connected to the CMMS. */ class SyntheticGeneratorCorrectnessTest : AbstractCorrectnessTest(measurementSystem) { private class RunningMeasurementSystem : MeasurementSystem, TestRule { override val runId: String by lazy { UUID.randomUUID().toString() } private lateinit var _testHarness: MeasurementConsumerSimulator + private lateinit var _reportingTestHarness: ReportingUserSimulator + override val testHarness: MeasurementConsumerSimulator get() = _testHarness + override val reportingTestHarness: ReportingUserSimulator + get() = _reportingTestHarness + private val channels = mutableListOf() override fun apply(base: Statement, description: Description): Statement { @@ -65,6 +73,7 @@ class SyntheticGeneratorCorrectnessTest : AbstractCorrectnessTest(measurementSys override fun evaluate() { try { _testHarness = createTestHarness() + _reportingTestHarness = createReportingTestHarness() base.evaluate() } finally { shutDownChannels() @@ -110,6 +119,33 @@ class SyntheticGeneratorCorrectnessTest : AbstractCorrectnessTest(measurementSys ) } + private fun createReportingTestHarness(): ReportingUserSimulator { + val publicApiChannel = + buildMutualTlsChannel( + TEST_CONFIG.reportingPublicApiTarget, + REPORTING_SIGNING_CERTS, + TEST_CONFIG.reportingPublicApiCertHost, + ) + .also { channels.add(it) } + .withDefaultDeadline(RPC_DEADLINE_DURATION) + + return ReportingUserSimulator( + measurementConsumerName = TEST_CONFIG.measurementConsumer, + dataProvidersClient = DataProvidersGrpcKt.DataProvidersCoroutineStub(publicApiChannel), + eventGroupsClient = + org.wfanet.measurement.reporting.v2alpha.EventGroupsGrpcKt.EventGroupsCoroutineStub( + publicApiChannel + ), + reportingSetsClient = + org.wfanet.measurement.reporting.v2alpha.ReportingSetsGrpcKt.ReportingSetsCoroutineStub( + publicApiChannel + ), + metricCalculationSpecsClient = + MetricCalculationSpecsGrpcKt.MetricCalculationSpecsCoroutineStub(publicApiChannel), + reportsClient = ReportsGrpcKt.ReportsCoroutineStub(publicApiChannel), + ) + } + private fun shutDownChannels() { for (channel in channels) { channel.shutdown() diff --git a/src/test/kotlin/org/wfanet/measurement/integration/k8s/correctness_test_config.tmpl.textproto b/src/test/kotlin/org/wfanet/measurement/integration/k8s/correctness_test_config.tmpl.textproto index a6b54b93d85..1242cd723ec 100644 --- a/src/test/kotlin/org/wfanet/measurement/integration/k8s/correctness_test_config.tmpl.textproto +++ b/src/test/kotlin/org/wfanet/measurement/integration/k8s/correctness_test_config.tmpl.textproto @@ -4,3 +4,5 @@ kingdom_public_api_target: "{kingdom_public_api_target}" kingdom_public_api_cert_host: "{kingdom_public_api_cert_host}" measurement_consumer: "{mc_name}" api_authentication_key: "{mc_api_key}" +reporting_public_api_target: "{reporting_public_api_target}" +reporting_public_api_cert_host: "{reporting_public_api_cert_host}" diff --git a/src/test/kotlin/org/wfanet/panelmatch/integration/k8s/EmptyClusterPanelMatchCorrectnessTest.kt b/src/test/kotlin/org/wfanet/panelmatch/integration/k8s/EmptyClusterPanelMatchCorrectnessTest.kt index 8c126916597..07de2db818b 100644 --- a/src/test/kotlin/org/wfanet/panelmatch/integration/k8s/EmptyClusterPanelMatchCorrectnessTest.kt +++ b/src/test/kotlin/org/wfanet/panelmatch/integration/k8s/EmptyClusterPanelMatchCorrectnessTest.kt @@ -494,7 +494,7 @@ class EmptyClusterPanelMatchCorrectnessTest : AbstractPanelMatchCorrectnessTest( private val LOCAL_K8S_PATH = Paths.get("src", "main", "k8s", "local") private val LOCAL_K8S_TESTING_PATH = LOCAL_K8S_PATH.resolve("testing") - private val CONFIG_FILES_PATH = LOCAL_K8S_TESTING_PATH.resolve("config_files") + private val CONFIG_FILES_PATH = LOCAL_K8S_TESTING_PATH.resolve("config_files_for_panel_match") private val LOCAL_K8S_PANELMATCH_PATH = Paths.get("src", "main", "k8s", "panelmatch", "local") private val PANELMATCH_CONFIG_FILES_PATH = LOCAL_K8S_PANELMATCH_PATH.resolve("config_files")